@indigoai-us/hq-cloud 6.18.87 → 6.18.89
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/dist/bin/sync-runner-company.d.ts.map +1 -1
- package/dist/bin/sync-runner-company.js +16 -0
- package/dist/bin/sync-runner-company.js.map +1 -1
- package/dist/bin/sync-runner.d.ts +5 -0
- package/dist/bin/sync-runner.d.ts.map +1 -1
- package/dist/bin/sync-runner.js.map +1 -1
- package/dist/cli/reconcile-skip-reason.d.ts +23 -0
- package/dist/cli/reconcile-skip-reason.d.ts.map +1 -0
- package/dist/cli/reconcile-skip-reason.js +60 -0
- package/dist/cli/reconcile-skip-reason.js.map +1 -0
- package/dist/cli/rescue-core.d.ts +16 -1
- package/dist/cli/rescue-core.d.ts.map +1 -1
- package/dist/cli/rescue-core.js +83 -27
- package/dist/cli/rescue-core.js.map +1 -1
- package/dist/cli/share.d.ts +31 -1
- package/dist/cli/share.d.ts.map +1 -1
- package/dist/cli/share.js +193 -28
- package/dist/cli/share.js.map +1 -1
- package/dist/cli/sync.d.ts +36 -2
- package/dist/cli/sync.d.ts.map +1 -1
- package/dist/cli/sync.js +135 -27
- package/dist/cli/sync.js.map +1 -1
- package/dist/local-link-warning.d.ts +48 -0
- package/dist/local-link-warning.d.ts.map +1 -0
- package/dist/local-link-warning.js +138 -0
- package/dist/local-link-warning.js.map +1 -0
- package/dist/object-io.d.ts +16 -1
- package/dist/object-io.d.ts.map +1 -1
- package/dist/object-io.js +25 -2
- package/dist/object-io.js.map +1 -1
- package/dist/outposts/cli-billing.d.ts +2 -0
- package/dist/outposts/cli-billing.d.ts.map +1 -1
- package/dist/outposts/cli-billing.js +13 -1
- package/dist/outposts/cli-billing.js.map +1 -1
- package/dist/outposts/client.d.ts +37 -6
- package/dist/outposts/client.d.ts.map +1 -1
- package/dist/outposts/client.js +100 -18
- package/dist/outposts/client.js.map +1 -1
- package/dist/outposts/command.d.ts +11 -5
- package/dist/outposts/command.d.ts.map +1 -1
- package/dist/outposts/command.js +945 -63
- package/dist/outposts/command.js.map +1 -1
- package/dist/outposts/company-outposts.d.ts +92 -0
- package/dist/outposts/company-outposts.d.ts.map +1 -0
- package/dist/outposts/company-outposts.js +194 -0
- package/dist/outposts/company-outposts.js.map +1 -0
- package/dist/outposts/company.d.ts +67 -9
- package/dist/outposts/company.d.ts.map +1 -1
- package/dist/outposts/company.js +211 -11
- package/dist/outposts/company.js.map +1 -1
- package/dist/outposts/errors.d.ts +7 -0
- package/dist/outposts/errors.d.ts.map +1 -1
- package/dist/outposts/errors.js +7 -0
- package/dist/outposts/errors.js.map +1 -1
- package/dist/outposts/host-deps.d.ts +2 -0
- package/dist/outposts/host-deps.d.ts.map +1 -1
- package/dist/outposts/host-deps.js +12 -0
- package/dist/outposts/host-deps.js.map +1 -1
- package/dist/s3.d.ts +7 -1
- package/dist/s3.d.ts.map +1 -1
- package/dist/s3.js +33 -12
- package/dist/s3.js.map +1 -1
- package/dist/sync-progress.d.ts.map +1 -1
- package/dist/sync-progress.js +3 -0
- package/dist/sync-progress.js.map +1 -1
- package/dist/vault-client.d.ts.map +1 -1
- package/dist/vault-client.js +5 -1
- package/dist/vault-client.js.map +1 -1
- package/package.json +1 -1
package/dist/outposts/command.js
CHANGED
|
@@ -9,15 +9,19 @@
|
|
|
9
9
|
*
|
|
10
10
|
* Personal Outposts are caller-scoped: hq-pro keys those `/outpost/*` calls on
|
|
11
11
|
* the caller's Cognito sub. `--company <slug>` switches the same provision and
|
|
12
|
-
* status commands onto the company Outpost
|
|
13
|
-
* a company job
|
|
14
|
-
* personal box; when omitted, personal calls target the caller's primary slot.
|
|
12
|
+
* status commands onto the company Outpost. `outposts jobs` registers, edits,
|
|
13
|
+
* pauses, resumes, deletes, and runs a company job. `--id <outpostId>` still
|
|
14
|
+
* selects a personal box; when omitted, personal calls target the caller's primary slot.
|
|
15
15
|
*
|
|
16
16
|
* Subcommands:
|
|
17
17
|
* outposts provision [--company] — provision a box (paid; requires --yes)
|
|
18
18
|
* outposts list — every personal Outpost you own
|
|
19
|
+
* outposts list --company - a company's Outposts (name, id, state, jobs)
|
|
20
|
+
* outposts create --company --name - another named company Outpost (billed to the company)
|
|
21
|
+
* --outpost <name|id> - picks a company Outpost on status, login, login-code
|
|
22
|
+
* and jobs register; optional when there is only one
|
|
19
23
|
* outposts status [--id|--company] — live detail for one box
|
|
20
|
-
* outposts jobs
|
|
24
|
+
* outposts jobs — register, edit, pause, resume, delete, run-now
|
|
21
25
|
* outposts exec <command...> — run a command on the box (sync/async/detach)
|
|
22
26
|
* outposts exec-stage — presign an input upload for an async command
|
|
23
27
|
* outposts exec-submit <cmd...> — fire-and-forget async submit
|
|
@@ -25,7 +29,7 @@
|
|
|
25
29
|
* outposts codex-enable [--id] — enable / retry Codex on the box
|
|
26
30
|
* outposts login [--id] — request a fresh login URL
|
|
27
31
|
* outposts login-code <code> — hand the box its Claude sign-in code
|
|
28
|
-
* outposts destroy [--id] --yes
|
|
32
|
+
* outposts destroy [--id | --company [--outpost]] [--yes] [--json] — tear a box down (destructive; confirmed first)
|
|
29
33
|
* outposts heartbeat — hidden; box-side systemd loop
|
|
30
34
|
* outposts session-host — hidden; box-side sessions for the desktop app
|
|
31
35
|
* outposts self-deploy — hidden; make THIS host an unmanaged outpost
|
|
@@ -38,7 +42,8 @@
|
|
|
38
42
|
import chalk from "chalk";
|
|
39
43
|
import * as fs from "node:fs";
|
|
40
44
|
import { OUTPOST_PRICE_CENTS } from "./billing.js";
|
|
41
|
-
import { buildCompanyJobDefinition, companyPayerLabel, companyProvisionProgress, CompanyTargetError, describeCompanySignIn, explainCompanyFailure, isCompanySlug, parseCompanySignIn, isProvisionOutcomeUnknown, mergeCompanyJobs, } from "./company.js";
|
|
45
|
+
import { buildCompanyJobDefinition, mergeCompanyJobEdit, companyPayerLabel, companyProvisionProgress, CompanyTargetError, JobDefinitionError, describeCompanySignIn, explainCompanyFailure, isCompanySlug, parseCompanySignIn, companyCreateContinues, isProvisionOutcomeUnknown, mergeCompanyJobs, provisionErrorSkipsStatusRead, } from "./company.js";
|
|
46
|
+
import { companyOutpostChargeLine, companyOutpostsUnreadableLine, CompanyOutpostSelectError, explainCompanyOutpostCreateFailure, explainCompanyOutpostSelect, isCompanyOutpostId, isOutpostName, LEGACY_OUTPOST_NAME, safeLifecycle, selectCompanyOutpost, } from "./company-outposts.js";
|
|
42
47
|
import { confirmChargeOrExit, confirmCompanyOutpostChargeOrExit, surfaceBillingBlocked, } from "./cli-billing.js";
|
|
43
48
|
import { EXEC_MAX_TIMEOUT_SECONDS, OutpostsClient, } from "./client.js";
|
|
44
49
|
import { OutpostExecWaitError, OutpostHttpError } from "./errors.js";
|
|
@@ -77,8 +82,39 @@ function fail(err) {
|
|
|
77
82
|
}
|
|
78
83
|
process.exit(1);
|
|
79
84
|
}
|
|
80
|
-
function
|
|
81
|
-
|
|
85
|
+
function companyFailureCode(err) {
|
|
86
|
+
if (err instanceof CompanyOutpostSelectError)
|
|
87
|
+
return err.reason;
|
|
88
|
+
if (err instanceof OutpostHttpError) {
|
|
89
|
+
if (err.code)
|
|
90
|
+
return err.code;
|
|
91
|
+
if (err.status === 403)
|
|
92
|
+
return "admin_required";
|
|
93
|
+
if (err.status === 401)
|
|
94
|
+
return "unauthorized";
|
|
95
|
+
}
|
|
96
|
+
if (err instanceof JobDefinitionError)
|
|
97
|
+
return "invalid_definition";
|
|
98
|
+
if (err instanceof CompanyTargetError)
|
|
99
|
+
return err.reason;
|
|
100
|
+
return "request_failed";
|
|
101
|
+
}
|
|
102
|
+
function failCompany(slug, err, intent, json) {
|
|
103
|
+
const message = err instanceof CompanyOutpostSelectError
|
|
104
|
+
? explainCompanyOutpostSelect(isCompanySlug(slug) ? slug : "that company", err)
|
|
105
|
+
: explainCompanyFailure(slug, err, intent, json?.jobId);
|
|
106
|
+
if (json) {
|
|
107
|
+
process.stdout.write(JSON.stringify({
|
|
108
|
+
ok: false,
|
|
109
|
+
action: json.action,
|
|
110
|
+
jobId: json.jobId,
|
|
111
|
+
code: companyFailureCode(err),
|
|
112
|
+
message,
|
|
113
|
+
}) + "\n");
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
console.error(message);
|
|
117
|
+
}
|
|
82
118
|
process.exit(1);
|
|
83
119
|
}
|
|
84
120
|
async function requireCompany(client, slug) {
|
|
@@ -96,6 +132,151 @@ async function requireCompany(client, slug) {
|
|
|
96
132
|
throw err;
|
|
97
133
|
}
|
|
98
134
|
}
|
|
135
|
+
/**
|
|
136
|
+
* The Outpost a company command acts on. `--outpost` is optional when the
|
|
137
|
+
* company has one Outpost. The legacy Outpost has no outpostId, so callers
|
|
138
|
+
* send none and older servers see the request they always did.
|
|
139
|
+
*/
|
|
140
|
+
async function requireCompanyOutpost(client, company, flag, command) {
|
|
141
|
+
return selectCompanyOutpost(await client.listCompanyOutposts(company.uid), flag, command);
|
|
142
|
+
}
|
|
143
|
+
/** Exit when --outpost is passed without --company. */
|
|
144
|
+
function rejectOutpostWithoutCompany(opts) {
|
|
145
|
+
if (opts.outpost !== undefined && opts.company === undefined) {
|
|
146
|
+
console.error(explainCompanyOutpostSelect("", new CompanyOutpostSelectError("needs_company")));
|
|
147
|
+
process.exit(1);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
/** " --outpost <name>" for next-step hints, only when the company has a choice to make. */
|
|
151
|
+
function outpostHint(outpost, passed) {
|
|
152
|
+
return passed !== undefined ? ` --outpost ${outpost.name}` : "";
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* `hq outposts list` is the only command that reads status after a rejected
|
|
156
|
+
* list query, and only to print the legacy row's state. The id from that
|
|
157
|
+
* read is not kept, so later commands still omit `outpostId`.
|
|
158
|
+
*/
|
|
159
|
+
async function legacyListWithLifecycle(client, companyUid, list) {
|
|
160
|
+
const row = list.outposts[0];
|
|
161
|
+
if (!list.legacy || list.outposts.length !== 1 || !row || row.lifecycle)
|
|
162
|
+
return list;
|
|
163
|
+
try {
|
|
164
|
+
const status = await client.companyStatus(companyUid);
|
|
165
|
+
const lifecycle = safeLifecycle(status.outpost?.lifecycle);
|
|
166
|
+
if (!lifecycle)
|
|
167
|
+
return list;
|
|
168
|
+
return { ...list, outposts: [{ ...row, lifecycle }] };
|
|
169
|
+
}
|
|
170
|
+
catch (err) {
|
|
171
|
+
// Only the server's coded "no Outpost" answer is an empty list. A codeless
|
|
172
|
+
// 404 is not proof, so it fails instead of suggesting a billed create.
|
|
173
|
+
if (err instanceof OutpostHttpError && err.status === 404 && err.code === "not_found") {
|
|
174
|
+
return { outposts: [], legacy: true };
|
|
175
|
+
}
|
|
176
|
+
throw err;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
async function listCompanyOutpostsCommand(client, slug, asJson) {
|
|
180
|
+
const company = await requireCompany(client, slug);
|
|
181
|
+
const list = await legacyListWithLifecycle(client, company.uid, await client.listCompanyOutposts(company.uid));
|
|
182
|
+
const unreadable = list.unreadable ?? 0;
|
|
183
|
+
// Every row unreadable is not "no Outpost": fail instead of an empty list or the create hint.
|
|
184
|
+
if (list.outposts.length === 0 && unreadable > 0) {
|
|
185
|
+
console.error(companyOutpostsUnreadableLine(company.slug));
|
|
186
|
+
process.exit(1);
|
|
187
|
+
}
|
|
188
|
+
if (asJson) {
|
|
189
|
+
process.stdout.write(JSON.stringify({ company: company.slug, legacy: list.legacy, outposts: list.outposts, ...(unreadable > 0 ? { unreadable } : {}) }, null, 2) + "\n");
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
if (list.outposts.length === 0) {
|
|
193
|
+
console.log(`${company.slug} does not have an Outpost yet.`);
|
|
194
|
+
console.log(`Next: hq outposts provision --company ${company.slug}`);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
const nameW = Math.max(4, ...list.outposts.map((o) => o.name.length));
|
|
198
|
+
const idW = Math.max(2, ...list.outposts.map((o) => (o.outpostId ?? "").length));
|
|
199
|
+
console.log(chalk.bold(["NAME".padEnd(nameW), "ID".padEnd(idW), "STATE"].join(" ")));
|
|
200
|
+
for (const o of list.outposts) {
|
|
201
|
+
console.log([
|
|
202
|
+
o.name.padEnd(nameW),
|
|
203
|
+
(o.outpostId ?? "").padEnd(idW),
|
|
204
|
+
o.lifecycle ?? "",
|
|
205
|
+
].join(" "));
|
|
206
|
+
}
|
|
207
|
+
if (list.legacy) {
|
|
208
|
+
console.log(chalk.dim(`${company.slug} has one Outpost, shown as ${LEGACY_OUTPOST_NAME}.`));
|
|
209
|
+
}
|
|
210
|
+
if (unreadable > 0) {
|
|
211
|
+
console.log(chalk.yellow(`${unreadable} more Outpost row(s) could not be read and are not shown.`));
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
async function createCompanyOutpostCommand(client, host, opts, wait = {}) {
|
|
215
|
+
const json = opts.json === true;
|
|
216
|
+
const name = opts.name?.trim() ?? "";
|
|
217
|
+
if (!isOutpostName(name)) {
|
|
218
|
+
console.error("An Outpost needs a name of 1 to 32 lowercase letters, digits or dashes, such as research. " +
|
|
219
|
+
`Next: re-run with --company ${isCompanySlug(opts.company) ? opts.company : "<slug>"} --name <name>.`);
|
|
220
|
+
if (json) {
|
|
221
|
+
writeCompanyProvisionJson({ ok: false, company: opts.company, state: "not_created", reason: "invalid_name" });
|
|
222
|
+
}
|
|
223
|
+
process.exit(1);
|
|
224
|
+
}
|
|
225
|
+
let company;
|
|
226
|
+
try {
|
|
227
|
+
company = await requireCompany(client, opts.company);
|
|
228
|
+
}
|
|
229
|
+
catch (err) {
|
|
230
|
+
if (err && typeof err === "object")
|
|
231
|
+
failedBeforeCreate.add(err);
|
|
232
|
+
throw err;
|
|
233
|
+
}
|
|
234
|
+
const charge = companyOutpostChargeLine(companyPayerLabel(company));
|
|
235
|
+
const unnamed = `The name ${name} is not sent. This server creates the company's main Outpost.`;
|
|
236
|
+
if (!opts.yes) {
|
|
237
|
+
if (!host.isStdinTty()) {
|
|
238
|
+
console.error(`Creating ${company.slug}'s main Outpost costs ${charge}. ${unnamed} ` +
|
|
239
|
+
`Next: re-run with --yes to confirm this charge.`);
|
|
240
|
+
if (json) {
|
|
241
|
+
writeCompanyProvisionJson({
|
|
242
|
+
ok: false,
|
|
243
|
+
company: company.slug,
|
|
244
|
+
state: "not_created",
|
|
245
|
+
reason: "confirmation_required",
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
process.exit(1);
|
|
249
|
+
}
|
|
250
|
+
// In --json the prompt goes to stderr so stdout stays one object.
|
|
251
|
+
const prompt = `Create ${company.slug}'s main Outpost? ${charge}. ${unnamed} Type yes to confirm: `;
|
|
252
|
+
if (json)
|
|
253
|
+
process.stderr.write(prompt);
|
|
254
|
+
else
|
|
255
|
+
process.stdout.write(prompt);
|
|
256
|
+
if (!(await host.confirm())) {
|
|
257
|
+
if (json) {
|
|
258
|
+
console.error("Nothing was created or billed.");
|
|
259
|
+
writeCompanyProvisionJson({
|
|
260
|
+
ok: false,
|
|
261
|
+
company: company.slug,
|
|
262
|
+
state: "not_created",
|
|
263
|
+
reason: "confirmation_declined",
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
else {
|
|
267
|
+
console.log("Nothing was created or billed.");
|
|
268
|
+
}
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
else {
|
|
273
|
+
provisionProgress(json, `Confirmed: ${charge}. ${unnamed}`);
|
|
274
|
+
}
|
|
275
|
+
await startCompanyOutpost(client, company, wait, {
|
|
276
|
+
json,
|
|
277
|
+
explainRefusal: (err) => explainCompanyOutpostCreateFailure(company.slug, err),
|
|
278
|
+
});
|
|
279
|
+
}
|
|
99
280
|
function printCompanyJobs(companySlug, jobs, healthAvailable) {
|
|
100
281
|
if (jobs.length === 0) {
|
|
101
282
|
console.log("No company jobs yet.");
|
|
@@ -108,116 +289,524 @@ function printCompanyJobs(companySlug, jobs, healthAvailable) {
|
|
|
108
289
|
console.log(["JOB", "LAST RUN", "NEXT RUN", "STATUS"].join(" "));
|
|
109
290
|
for (const job of jobs) {
|
|
110
291
|
console.log([job.jobId, job.lastRunAt ?? "none", job.nextRunAt ?? "none", job.status].join(" "));
|
|
292
|
+
if (job.message)
|
|
293
|
+
console.log(` ${job.message}`);
|
|
294
|
+
if (job.nextStep)
|
|
295
|
+
console.log(` Next: ${job.nextStep}`);
|
|
111
296
|
}
|
|
112
297
|
}
|
|
113
298
|
const COMPANY_PROVISION_POLL_MS = 5_000;
|
|
114
299
|
// The create Lambda's own timeout is 600s.
|
|
115
300
|
const COMPANY_PROVISION_MAX_WAIT_MS = 600_000;
|
|
116
301
|
const COMPANY_PROVISION_PROGRESS_EVERY_MS = 30_000;
|
|
117
|
-
|
|
118
|
-
const
|
|
302
|
+
function writeCompanyProvisionJson(body) {
|
|
303
|
+
const payload = {
|
|
304
|
+
ok: body.ok,
|
|
305
|
+
company: body.company,
|
|
306
|
+
state: body.state,
|
|
307
|
+
};
|
|
308
|
+
if (body.outpostId)
|
|
309
|
+
payload.outpostId = body.outpostId;
|
|
310
|
+
if (body.code)
|
|
311
|
+
payload.code = body.code;
|
|
312
|
+
if (body.reason)
|
|
313
|
+
payload.reason = body.reason;
|
|
314
|
+
process.stdout.write(JSON.stringify(payload) + "\n");
|
|
315
|
+
}
|
|
316
|
+
/** Errors thrown before the create request was sent, so nothing can exist. */
|
|
317
|
+
const failedBeforeCreate = new WeakSet();
|
|
318
|
+
function companyProvisionFailureJson(company, err) {
|
|
319
|
+
if (err instanceof CompanyTargetError) {
|
|
320
|
+
return JSON.stringify({
|
|
321
|
+
ok: false,
|
|
322
|
+
company,
|
|
323
|
+
state: "not_created",
|
|
324
|
+
reason: err.reason,
|
|
325
|
+
}) + "\n";
|
|
326
|
+
}
|
|
327
|
+
const code = err instanceof OutpostHttpError ? err.code : undefined;
|
|
328
|
+
const reason = code ?? "request_failed";
|
|
329
|
+
let state = "unconfirmed";
|
|
330
|
+
if (err && typeof err === "object" && failedBeforeCreate.has(err))
|
|
331
|
+
state = "not_created";
|
|
332
|
+
else if (code === "already_exists" || code === "billing_required")
|
|
333
|
+
state = "blocked";
|
|
334
|
+
else if (code === "feature_disabled" ||
|
|
335
|
+
code === "unavailable" ||
|
|
336
|
+
code === "dependencies_unavailable" ||
|
|
337
|
+
code === "unauthorized" ||
|
|
338
|
+
code === "forbidden" ||
|
|
339
|
+
code === "admin_required" ||
|
|
340
|
+
(typeof code === "string" && code.startsWith("invalid_"))) {
|
|
341
|
+
state = "not_created";
|
|
342
|
+
}
|
|
343
|
+
return JSON.stringify({
|
|
344
|
+
ok: false,
|
|
345
|
+
company,
|
|
346
|
+
state,
|
|
347
|
+
...(code ? { code } : {}),
|
|
348
|
+
reason,
|
|
349
|
+
}) + "\n";
|
|
350
|
+
}
|
|
351
|
+
function provisionProgress(json, line) {
|
|
352
|
+
if (json)
|
|
353
|
+
console.error(chalk.dim(line));
|
|
354
|
+
else
|
|
355
|
+
console.log(chalk.dim(line));
|
|
356
|
+
}
|
|
357
|
+
function correlationFrom(outpost, err) {
|
|
358
|
+
const fromError = err instanceof OutpostHttpError ? err : undefined;
|
|
359
|
+
const attemptId = typeof outpost?.attemptId === "string" ? outpost.attemptId : fromError?.attemptId;
|
|
360
|
+
const operationId = typeof outpost?.operationId === "string" ? outpost.operationId : fromError?.operationId;
|
|
361
|
+
return {
|
|
362
|
+
...(attemptId ? { attemptId } : {}),
|
|
363
|
+
...(operationId ? { operationId } : {}),
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
async function provisionCompanyOutpost(client, slug, yes, wait = {}, json = false) {
|
|
367
|
+
let company;
|
|
368
|
+
try {
|
|
369
|
+
company = await requireCompany(client, slug);
|
|
370
|
+
}
|
|
371
|
+
catch (err) {
|
|
372
|
+
if (err && typeof err === "object")
|
|
373
|
+
failedBeforeCreate.add(err);
|
|
374
|
+
throw err;
|
|
375
|
+
}
|
|
119
376
|
confirmCompanyOutpostChargeOrExit({
|
|
120
377
|
slug: company.slug,
|
|
121
378
|
payerLabel: companyPayerLabel(company),
|
|
122
379
|
yes,
|
|
380
|
+
json,
|
|
123
381
|
});
|
|
382
|
+
await startCompanyOutpost(client, company, wait, { json });
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Send the create, then report what status shows. `provision --company` and
|
|
386
|
+
* `create --company --name` both use this, so neither prints "not created"
|
|
387
|
+
* for a create that may still be running.
|
|
388
|
+
*
|
|
389
|
+
* A refusal from {@link provisionErrorSkipsStatusRead} is a real answer:
|
|
390
|
+
* `explainRefusal` may give its plain line (not in `--json`), otherwise it
|
|
391
|
+
* is rethrown for the command's error handler. Any other error, such as a
|
|
392
|
+
* codeless 5xx (API Gateway's 30s cut-off on a create that keeps running),
|
|
393
|
+
* `503 outcome_unknown`, a coded 5xx, a dropped connection or an unreadable
|
|
394
|
+
* body, says nothing final about whether the box exists, so status is read.
|
|
395
|
+
* With `json`, progress goes to stderr and stdout gets one object.
|
|
396
|
+
*/
|
|
397
|
+
async function startCompanyOutpost(client, company, wait, opts = {}) {
|
|
398
|
+
const json = opts.json === true;
|
|
124
399
|
let outpost;
|
|
400
|
+
let provisionError;
|
|
125
401
|
try {
|
|
126
402
|
outpost = (await client.provisionCompany(company.uid)).outpost;
|
|
127
403
|
}
|
|
128
404
|
catch (err) {
|
|
129
|
-
// A
|
|
130
|
-
//
|
|
131
|
-
//
|
|
132
|
-
if (
|
|
405
|
+
// A refusal or a coded "nothing changed" answer is final.
|
|
406
|
+
// Anything else may have created the box. Poll status before any failure text.
|
|
407
|
+
// A failed read, a single 404, or a timeout stays unconfirmed and keeps this error.
|
|
408
|
+
if (provisionErrorSkipsStatusRead(err)) {
|
|
409
|
+
const plain = opts.json ? undefined : opts.explainRefusal?.(err);
|
|
410
|
+
if (plain) {
|
|
411
|
+
console.error(plain);
|
|
412
|
+
process.exit(1);
|
|
413
|
+
}
|
|
133
414
|
throw err;
|
|
134
|
-
|
|
415
|
+
}
|
|
416
|
+
provisionError = err;
|
|
417
|
+
const line = isProvisionOutcomeUnknown(err)
|
|
418
|
+
? "The create request did not return a result in time. Checking status..."
|
|
419
|
+
: "The create request returned an error. Checking status...";
|
|
420
|
+
provisionProgress(json, line);
|
|
135
421
|
}
|
|
136
|
-
const
|
|
137
|
-
const finished = await waitForCompanyOutpost(client, company.uid, outpost, wait
|
|
422
|
+
const code = provisionError instanceof OutpostHttpError ? provisionError.code : undefined;
|
|
423
|
+
const finished = await waitForCompanyOutpost(client, company.uid, outpost, wait, {
|
|
424
|
+
json,
|
|
425
|
+
correlation: correlationFrom(outpost, provisionError),
|
|
426
|
+
trustInitial: outpost !== undefined,
|
|
427
|
+
announceInProgress: provisionError !== undefined,
|
|
428
|
+
});
|
|
429
|
+
// The id goes into a terminal line and JSON, so only a printable one is shown.
|
|
430
|
+
const rawId = finished.outpostId ?? outpost?.outpostId;
|
|
431
|
+
const id = typeof rawId === "string" && isCompanyOutpostId(rawId) ? rawId : undefined;
|
|
138
432
|
const label = `${company.slug}'s Outpost${id ? ` (${id})` : ""}`;
|
|
139
433
|
if (finished.state === "ready") {
|
|
434
|
+
if (json) {
|
|
435
|
+
writeCompanyProvisionJson({
|
|
436
|
+
ok: true,
|
|
437
|
+
company: company.slug,
|
|
438
|
+
state: "ready",
|
|
439
|
+
...(id ? { outpostId: id } : {}),
|
|
440
|
+
});
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
140
443
|
console.log(chalk.green(`${label} is ready.`));
|
|
141
444
|
console.log(chalk.dim(`Next: hq outposts status --company ${company.slug}`));
|
|
142
445
|
return;
|
|
143
446
|
}
|
|
144
447
|
if (finished.state === "blocked") {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
:
|
|
448
|
+
const notCreated = finished.reason === "not_started";
|
|
449
|
+
if (json) {
|
|
450
|
+
writeCompanyProvisionJson({
|
|
451
|
+
ok: false,
|
|
452
|
+
company: company.slug,
|
|
453
|
+
state: notCreated ? "not_created" : "blocked",
|
|
454
|
+
...(id ? { outpostId: id } : {}),
|
|
455
|
+
...(code ? { code } : {}),
|
|
456
|
+
...(finished.reason ? { reason: finished.reason } : {}),
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
else if (notCreated) {
|
|
460
|
+
console.error(`${label} was not created; nothing was built. Next: re-run hq outposts provision --company ${company.slug} to try again.`);
|
|
461
|
+
}
|
|
462
|
+
else if (finished.reason === "billing_required") {
|
|
463
|
+
console.error(`Billing for ${company.slug} is blocked, so the Outpost could not finish provisioning. Next: check the company's billing in the console, then run hq outposts status --company ${company.slug}.`);
|
|
464
|
+
}
|
|
465
|
+
else {
|
|
466
|
+
console.error(`${label} stopped while provisioning and needs attention. Next: hq outposts status --company ${company.slug}, and contact support before retrying.`);
|
|
467
|
+
}
|
|
150
468
|
// exitCode, not exit(): the action's catch must not add a second message.
|
|
151
469
|
process.exitCode = 1;
|
|
152
470
|
return;
|
|
153
471
|
}
|
|
154
472
|
if (finished.state === "unconfirmed") {
|
|
155
|
-
console.error(
|
|
473
|
+
console.error(finished.signInFailed
|
|
474
|
+
? `The result of the create request could not be confirmed because the status check needs you to sign in to HQ again. Next: run hq login, then run hq outposts status --company ${company.slug} first, before retrying.`
|
|
475
|
+
: `The result of the create request could not be confirmed. Next: run hq outposts status --company ${company.slug} first, before retrying.`);
|
|
476
|
+
if (json) {
|
|
477
|
+
writeCompanyProvisionJson({
|
|
478
|
+
ok: false,
|
|
479
|
+
company: company.slug,
|
|
480
|
+
state: "unconfirmed",
|
|
481
|
+
...(id ? { outpostId: id } : {}),
|
|
482
|
+
...(code ? { code } : {}),
|
|
483
|
+
reason: code ?? "unconfirmed",
|
|
484
|
+
});
|
|
485
|
+
}
|
|
156
486
|
process.exitCode = 1;
|
|
157
487
|
return;
|
|
158
488
|
}
|
|
489
|
+
if (json) {
|
|
490
|
+
writeCompanyProvisionJson({
|
|
491
|
+
ok: true,
|
|
492
|
+
company: company.slug,
|
|
493
|
+
state: "pending",
|
|
494
|
+
...(id ? { outpostId: id } : {}),
|
|
495
|
+
});
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
159
498
|
console.log(chalk.yellow(`Provisioning started for ${label}; it is not ready yet. Check it with: hq outposts status --company ${company.slug}`));
|
|
160
499
|
}
|
|
500
|
+
function rowBelongsToCreate(outpost, poll) {
|
|
501
|
+
if (!outpost)
|
|
502
|
+
return false;
|
|
503
|
+
if (poll.correlation.attemptId || poll.correlation.operationId) {
|
|
504
|
+
return companyCreateContinues(outpost, poll.correlation);
|
|
505
|
+
}
|
|
506
|
+
if (poll.trustInitial)
|
|
507
|
+
return true;
|
|
508
|
+
return companyCreateContinues(outpost, {});
|
|
509
|
+
}
|
|
161
510
|
/**
|
|
162
511
|
* Poll company status until the box is ready or blocked, or the wait runs out.
|
|
163
|
-
*
|
|
512
|
+
* Any status-read error, or a row that is not this create, is "not yet".
|
|
513
|
+
* Only an adopted row can end the wait.
|
|
164
514
|
*/
|
|
165
|
-
async function waitForCompanyOutpost(client, companyUid, initial, wait) {
|
|
515
|
+
async function waitForCompanyOutpost(client, companyUid, initial, wait, poll) {
|
|
166
516
|
const pollMs = wait.pollMs ?? COMPANY_PROVISION_POLL_MS;
|
|
167
517
|
const maxWaitMs = wait.maxWaitMs ?? COMPANY_PROVISION_MAX_WAIT_MS;
|
|
168
518
|
const pause = wait.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
169
519
|
const now = wait.now ?? Date.now;
|
|
520
|
+
let outpostId = typeof initial?.outpostId === "string" ? initial.outpostId : undefined;
|
|
170
521
|
let progress = initial ? companyProvisionProgress(initial) : undefined;
|
|
171
|
-
|
|
172
|
-
|
|
522
|
+
let announced = false;
|
|
523
|
+
const announce = (state) => {
|
|
524
|
+
if (announced || !poll.announceInProgress || state.state !== "pending")
|
|
525
|
+
return;
|
|
526
|
+
announced = true;
|
|
527
|
+
const line = "Status shows a create in progress.";
|
|
528
|
+
if (poll.json)
|
|
529
|
+
console.error(line);
|
|
530
|
+
else
|
|
531
|
+
console.log(line);
|
|
532
|
+
};
|
|
533
|
+
if (progress && progress.state !== "pending") {
|
|
534
|
+
return outpostId ? { ...progress, outpostId } : progress;
|
|
535
|
+
}
|
|
536
|
+
if (progress?.state === "pending")
|
|
537
|
+
announce(progress);
|
|
173
538
|
const started = now();
|
|
174
539
|
let lastLifecycle = progress?.state === "pending" ? progress.lifecycle : undefined;
|
|
175
540
|
let lastProgressAt = started;
|
|
176
541
|
let anyRead = initial !== undefined;
|
|
177
542
|
for (;;) {
|
|
178
543
|
if (now() - started >= maxWaitMs) {
|
|
179
|
-
|
|
544
|
+
if (anyRead && progress)
|
|
545
|
+
return outpostId ? { ...progress, outpostId } : progress;
|
|
546
|
+
return outpostId ? { state: "unconfirmed", outpostId } : { state: "unconfirmed" };
|
|
180
547
|
}
|
|
181
548
|
await pause(pollMs);
|
|
182
549
|
try {
|
|
550
|
+
// No outpostId: the deployed server rejects it with 400 invalid_body
|
|
551
|
+
// until hq-pro-agents #1217 ships. Rows are matched by attempt id instead.
|
|
183
552
|
const status = await client.companyStatus(companyUid);
|
|
553
|
+
if (!rowBelongsToCreate(status.outpost, poll))
|
|
554
|
+
continue;
|
|
184
555
|
progress = companyProvisionProgress(status.outpost);
|
|
185
556
|
anyRead = true;
|
|
557
|
+
if (typeof status.outpost?.outpostId === "string")
|
|
558
|
+
outpostId = status.outpost.outpostId;
|
|
186
559
|
}
|
|
187
560
|
catch (err) {
|
|
188
|
-
//
|
|
189
|
-
//
|
|
190
|
-
if (err instanceof OutpostHttpError &&
|
|
191
|
-
|
|
192
|
-
err.status < 500 &&
|
|
193
|
-
err.status !== 404) {
|
|
194
|
-
throw err;
|
|
561
|
+
// A create was already sent, so no status-read error proves what happened.
|
|
562
|
+
// A sign-in failure will not fix itself: stop now, still unconfirmed.
|
|
563
|
+
if (err instanceof OutpostHttpError && (err.status === 401 || err.status === 403)) {
|
|
564
|
+
return { state: "unconfirmed", signInFailed: true, ...(outpostId ? { outpostId } : {}) };
|
|
195
565
|
}
|
|
566
|
+
// Anything else is "not yet"; the timeout ends unconfirmed with the create's own error.
|
|
196
567
|
continue;
|
|
197
568
|
}
|
|
198
|
-
if (progress.state !== "pending")
|
|
199
|
-
return progress;
|
|
569
|
+
if (!progress || progress.state !== "pending") {
|
|
570
|
+
return outpostId && progress ? { ...progress, outpostId } : progress ?? { state: "unconfirmed" };
|
|
571
|
+
}
|
|
572
|
+
announce(progress);
|
|
200
573
|
const elapsed = Math.round((now() - started) / 1000);
|
|
201
574
|
if (progress.lifecycle !== lastLifecycle) {
|
|
202
575
|
lastLifecycle = progress.lifecycle;
|
|
203
576
|
lastProgressAt = now();
|
|
204
|
-
|
|
577
|
+
provisionProgress(poll.json, `Outpost is ${progress.lifecycle ?? "being created"} (${elapsed}s).`);
|
|
205
578
|
}
|
|
206
579
|
else if (now() - lastProgressAt >= COMPANY_PROVISION_PROGRESS_EVERY_MS) {
|
|
207
580
|
lastProgressAt = now();
|
|
208
|
-
|
|
581
|
+
provisionProgress(poll.json, `Still ${progress.lifecycle ?? "provisioning"} (${elapsed}s).`);
|
|
209
582
|
}
|
|
210
583
|
}
|
|
211
584
|
}
|
|
212
|
-
|
|
585
|
+
function writeCompanyDestroyJson(body) {
|
|
586
|
+
const payload = {
|
|
587
|
+
ok: body.ok,
|
|
588
|
+
company: body.company,
|
|
589
|
+
outpostId: body.outpostId ?? null,
|
|
590
|
+
state: body.state,
|
|
591
|
+
};
|
|
592
|
+
if (body.code)
|
|
593
|
+
payload.code = body.code;
|
|
594
|
+
if (body.reason)
|
|
595
|
+
payload.reason = body.reason;
|
|
596
|
+
process.stdout.write(JSON.stringify(payload) + "\n");
|
|
597
|
+
}
|
|
598
|
+
/**
|
|
599
|
+
* A destroy error the server sent as a final answer: a coded 4xx refusal
|
|
600
|
+
* such as `admin_required`, `reconciliation_required` or `not_found`.
|
|
601
|
+
* Anything else (every 5xx, coded or not, a codeless 4xx, a timeout, a
|
|
602
|
+
* dropped connection, `outcome_unknown`) may have started the teardown, so
|
|
603
|
+
* company status is read before anything is reported. The destroy itself is
|
|
604
|
+
* never retried.
|
|
605
|
+
*/
|
|
606
|
+
function destroyErrorIsFinal(err) {
|
|
607
|
+
if (!(err instanceof OutpostHttpError))
|
|
608
|
+
return false;
|
|
609
|
+
if (err.code === undefined || err.code === "outcome_unknown")
|
|
610
|
+
return false;
|
|
611
|
+
return err.status >= 400 && err.status < 500;
|
|
612
|
+
}
|
|
613
|
+
/**
|
|
614
|
+
* Read company status until the Outpost reads destroyed, or the wait runs out.
|
|
615
|
+
* The first read is immediate. A coded 404 `not_found` means the row is gone,
|
|
616
|
+
* which is destroyed. `destroying` keeps waiting. Any other lifecycle stops
|
|
617
|
+
* the wait as not confirmed. Other read errors are "not yet".
|
|
618
|
+
*/
|
|
619
|
+
async function waitForCompanyDestroy(client, companyUid, outpostId, wait, json) {
|
|
620
|
+
const pollMs = wait.pollMs ?? COMPANY_PROVISION_POLL_MS;
|
|
621
|
+
const maxWaitMs = wait.maxWaitMs ?? COMPANY_PROVISION_MAX_WAIT_MS;
|
|
622
|
+
const pause = wait.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
623
|
+
const now = wait.now ?? Date.now;
|
|
624
|
+
const started = now();
|
|
625
|
+
let sawDestroying = false;
|
|
626
|
+
let lastProgressAt = started;
|
|
627
|
+
for (let first = true;; first = false) {
|
|
628
|
+
if (!first) {
|
|
629
|
+
if (now() - started >= maxWaitMs) {
|
|
630
|
+
return sawDestroying ? { state: "destroying" } : { state: "unconfirmed" };
|
|
631
|
+
}
|
|
632
|
+
await pause(pollMs);
|
|
633
|
+
}
|
|
634
|
+
let lifecycle;
|
|
635
|
+
try {
|
|
636
|
+
const status = await client.companyStatus(companyUid, outpostId);
|
|
637
|
+
lifecycle = safeLifecycle(status.outpost?.lifecycle);
|
|
638
|
+
}
|
|
639
|
+
catch (err) {
|
|
640
|
+
// The server's coded "no active Outpost" is gone. A codeless 404 is not proof.
|
|
641
|
+
if (err instanceof OutpostHttpError && err.status === 404 && err.code === "not_found") {
|
|
642
|
+
return { state: "destroyed" };
|
|
643
|
+
}
|
|
644
|
+
// A sign-in failure will not fix itself: stop now, still unconfirmed.
|
|
645
|
+
if (err instanceof OutpostHttpError && (err.status === 401 || err.status === 403)) {
|
|
646
|
+
return { state: "unconfirmed", signInFailed: true };
|
|
647
|
+
}
|
|
648
|
+
// Anything else is "not yet"; the timeout ends unconfirmed.
|
|
649
|
+
continue;
|
|
650
|
+
}
|
|
651
|
+
if (lifecycle === "destroyed")
|
|
652
|
+
return { state: "destroyed" };
|
|
653
|
+
if (lifecycle !== "destroying") {
|
|
654
|
+
return lifecycle ? { state: "unconfirmed", lifecycle } : { state: "unconfirmed" };
|
|
655
|
+
}
|
|
656
|
+
const elapsed = Math.round((now() - started) / 1000);
|
|
657
|
+
if (!sawDestroying) {
|
|
658
|
+
sawDestroying = true;
|
|
659
|
+
lastProgressAt = now();
|
|
660
|
+
provisionProgress(json, `Outpost is destroying (${elapsed}s).`);
|
|
661
|
+
}
|
|
662
|
+
else if (now() - lastProgressAt >= COMPANY_PROVISION_PROGRESS_EVERY_MS) {
|
|
663
|
+
lastProgressAt = now();
|
|
664
|
+
provisionProgress(json, `Still destroying (${elapsed}s).`);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
/** Plain refusal for destroy. Nothing was destroyed. */
|
|
669
|
+
function failCompanyDestroy(slug, outpostId, err, json) {
|
|
670
|
+
const reason = err instanceof CompanyOutpostSelectError
|
|
671
|
+
? explainCompanyOutpostSelect(isCompanySlug(slug) ? slug : "that company", err)
|
|
672
|
+
: explainCompanyFailure(slug, err, "destroy");
|
|
673
|
+
console.error(reason);
|
|
674
|
+
if (json) {
|
|
675
|
+
writeCompanyDestroyJson({
|
|
676
|
+
ok: false,
|
|
677
|
+
company: slug,
|
|
678
|
+
...(outpostId ? { outpostId } : {}),
|
|
679
|
+
state: "not_destroyed",
|
|
680
|
+
code: companyFailureCode(err),
|
|
681
|
+
reason,
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
process.exit(1);
|
|
685
|
+
}
|
|
686
|
+
/**
|
|
687
|
+
* `hq outposts destroy --company <slug> [--outpost <name|id>]`.
|
|
688
|
+
*
|
|
689
|
+
* Never touches the personal Outpost. Nothing is sent until the person
|
|
690
|
+
* confirms: at a terminal they type the Outpost name or yes; without one,
|
|
691
|
+
* `--yes` is required. With `--json`, only `--yes` confirms. After the call, company status decides what is
|
|
692
|
+
* reported: destroyed, still destroying, or not confirmed.
|
|
693
|
+
*/
|
|
694
|
+
async function destroyCompanyOutpostCommand(client, host, opts, wait = {}) {
|
|
695
|
+
const json = opts.json === true;
|
|
696
|
+
// A script never confirms at a prompt: with --json, only --yes destroys.
|
|
697
|
+
if (json && !opts.yes) {
|
|
698
|
+
const reason = "--json needs --yes to destroy an Outpost. Nothing was sent. Next: re-run with --yes to confirm.";
|
|
699
|
+
console.error(reason);
|
|
700
|
+
writeCompanyDestroyJson({
|
|
701
|
+
ok: false,
|
|
702
|
+
company: opts.company,
|
|
703
|
+
state: "not_destroyed",
|
|
704
|
+
code: "confirmation_required",
|
|
705
|
+
reason,
|
|
706
|
+
});
|
|
707
|
+
process.exit(1);
|
|
708
|
+
}
|
|
709
|
+
let company;
|
|
710
|
+
let target;
|
|
711
|
+
try {
|
|
712
|
+
company = await requireCompany(client, opts.company);
|
|
713
|
+
target = await requireCompanyOutpost(client, company, opts.outpost, "hq outposts destroy");
|
|
714
|
+
}
|
|
715
|
+
catch (err) {
|
|
716
|
+
failCompanyDestroy(opts.company, undefined, err, json);
|
|
717
|
+
}
|
|
718
|
+
const id = target.outpostId;
|
|
719
|
+
const label = `${company.slug}'s Outpost ${target.name}${id ? ` (${id})` : ""}`;
|
|
720
|
+
if (!opts.yes) {
|
|
721
|
+
if (!host.isStdinTty()) {
|
|
722
|
+
console.error(`Destroying ${label} needs confirmation. Nothing was sent. Next: re-run with --yes to confirm.`);
|
|
723
|
+
process.exit(1);
|
|
724
|
+
}
|
|
725
|
+
process.stdout.write("This permanently destroys an Outpost and everything on it.\n" +
|
|
726
|
+
` Company: ${company.slug}\n` +
|
|
727
|
+
` Outpost: ${target.name}\n` +
|
|
728
|
+
` Id: ${id ?? "none (the company's only Outpost)"}\n` +
|
|
729
|
+
`Type the Outpost name (${target.name}) or yes to confirm: `);
|
|
730
|
+
const answer = host.readAnswer
|
|
731
|
+
? (await host.readAnswer()).trim()
|
|
732
|
+
: (await host.confirm()) ? "yes" : "";
|
|
733
|
+
if (answer !== target.name && !/^yes$/i.test(answer)) {
|
|
734
|
+
console.log("Nothing was destroyed.");
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
else {
|
|
739
|
+
provisionProgress(json, `Confirmed with --yes: destroying ${label}.`);
|
|
740
|
+
}
|
|
741
|
+
let destroyError;
|
|
742
|
+
try {
|
|
743
|
+
await client.destroyCompany(company.uid, id);
|
|
744
|
+
}
|
|
745
|
+
catch (err) {
|
|
746
|
+
if (destroyErrorIsFinal(err))
|
|
747
|
+
failCompanyDestroy(company.slug, id, err, json);
|
|
748
|
+
destroyError = err;
|
|
749
|
+
provisionProgress(json, isProvisionOutcomeUnknown(err)
|
|
750
|
+
? "The destroy request did not return a result in time. Checking status..."
|
|
751
|
+
: "The destroy request returned an error. Checking status...");
|
|
752
|
+
}
|
|
753
|
+
const finished = await waitForCompanyDestroy(client, company.uid, id, wait, json);
|
|
754
|
+
const code = destroyError instanceof OutpostHttpError ? destroyError.code : undefined;
|
|
755
|
+
const statusNext = `hq outposts status --company ${company.slug}${outpostHint(target, opts.outpost)}`;
|
|
756
|
+
if (finished.state === "destroyed") {
|
|
757
|
+
if (json) {
|
|
758
|
+
writeCompanyDestroyJson({ ok: true, company: company.slug, ...(id ? { outpostId: id } : {}), state: "destroyed" });
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
console.log(chalk.green(`${label} is destroyed.`));
|
|
762
|
+
return;
|
|
763
|
+
}
|
|
764
|
+
if (finished.state === "destroying") {
|
|
765
|
+
const reason = `${label} is still being destroyed. Next: check it in a few minutes with ${statusNext}`;
|
|
766
|
+
if (json) {
|
|
767
|
+
console.error(reason);
|
|
768
|
+
writeCompanyDestroyJson({
|
|
769
|
+
ok: true,
|
|
770
|
+
company: company.slug,
|
|
771
|
+
...(id ? { outpostId: id } : {}),
|
|
772
|
+
state: "destroying",
|
|
773
|
+
...(code ? { code } : {}),
|
|
774
|
+
reason,
|
|
775
|
+
});
|
|
776
|
+
return;
|
|
777
|
+
}
|
|
778
|
+
console.log(chalk.yellow(reason));
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
const reason = finished.signInFailed
|
|
782
|
+
? `The destroy could not be confirmed because the status check needs you to sign in to HQ again. Next: run hq login, then ${statusNext} before retrying.`
|
|
783
|
+
: finished.lifecycle
|
|
784
|
+
? `The destroy could not be confirmed: status shows ${label} as ${finished.lifecycle}. Next: ${statusNext} before retrying.`
|
|
785
|
+
: `The destroy could not be confirmed. Next: ${statusNext} before retrying.`;
|
|
786
|
+
console.error(reason);
|
|
787
|
+
if (json) {
|
|
788
|
+
writeCompanyDestroyJson({
|
|
789
|
+
ok: false,
|
|
790
|
+
company: company.slug,
|
|
791
|
+
...(id ? { outpostId: id } : {}),
|
|
792
|
+
state: "unconfirmed",
|
|
793
|
+
code: code ?? "unconfirmed",
|
|
794
|
+
reason,
|
|
795
|
+
});
|
|
796
|
+
}
|
|
797
|
+
// exitCode, not exit(): the action's catch must not add a second message.
|
|
798
|
+
process.exitCode = 1;
|
|
799
|
+
}
|
|
800
|
+
async function showCompanyStatus(client, slug, asJson, outpostFlag) {
|
|
213
801
|
const company = await requireCompany(client, slug);
|
|
214
|
-
const
|
|
802
|
+
const target = await requireCompanyOutpost(client, company, outpostFlag, "hq outposts status");
|
|
803
|
+
const status = await client.companyStatus(company.uid, target.outpostId);
|
|
215
804
|
const outpost = status.outpost ?? {};
|
|
216
|
-
const registry = await client.listCompanyJobs(company.uid);
|
|
805
|
+
const registry = await client.listCompanyJobs(company.uid, target.outpostId);
|
|
217
806
|
let health = null;
|
|
218
807
|
let healthAvailable = false;
|
|
219
808
|
try {
|
|
220
|
-
health = await client.companyJobHealth(company.uid);
|
|
809
|
+
health = await client.companyJobHealth(company.uid, target.outpostId);
|
|
221
810
|
healthAvailable = true;
|
|
222
811
|
}
|
|
223
812
|
catch (err) {
|
|
@@ -228,7 +817,7 @@ async function showCompanyStatus(client, slug, asJson) {
|
|
|
228
817
|
const jobs = mergeCompanyJobs(registry, health);
|
|
229
818
|
let signIn = { signedIn: false, state: "unknown" };
|
|
230
819
|
try {
|
|
231
|
-
signIn = parseCompanySignIn(await client.companySignIn(company.uid, "status"));
|
|
820
|
+
signIn = parseCompanySignIn(await client.companySignIn(company.uid, "status", undefined, target.outpostId));
|
|
232
821
|
}
|
|
233
822
|
catch (err) {
|
|
234
823
|
// An older server has no sign-in route: unknown. A non-admin is told why.
|
|
@@ -239,6 +828,7 @@ async function showCompanyStatus(client, slug, asJson) {
|
|
|
239
828
|
if (asJson) {
|
|
240
829
|
process.stdout.write(JSON.stringify({
|
|
241
830
|
company: { slug: company.slug, uid: company.uid, ...(company.name ? { name: company.name } : {}) },
|
|
831
|
+
outpostName: target.name,
|
|
242
832
|
outpost,
|
|
243
833
|
jobs,
|
|
244
834
|
jobHealth: healthAvailable ? "available" : "unavailable",
|
|
@@ -254,7 +844,7 @@ async function showCompanyStatus(client, slug, asJson) {
|
|
|
254
844
|
: "";
|
|
255
845
|
console.log(`Company ${company.slug}${company.name ? ` (${company.name})` : ""}`);
|
|
256
846
|
console.log(`Payer ${companyPayerLabel(company)}`);
|
|
257
|
-
console.log(`Outpost ${outpostId}`);
|
|
847
|
+
console.log(`Outpost ${target.name}${outpostId ? ` (${outpostId})` : ""}`);
|
|
258
848
|
console.log(`State ${lifecycle}`);
|
|
259
849
|
if (instance)
|
|
260
850
|
console.log(`Instance ${instance}`);
|
|
@@ -267,13 +857,14 @@ async function registerCompanyJobCommand(client, opts) {
|
|
|
267
857
|
const slug = opts.company ?? "";
|
|
268
858
|
const definition = buildCompanyJobDefinition(opts);
|
|
269
859
|
const company = await requireCompany(client, slug);
|
|
860
|
+
const target = await requireCompanyOutpost(client, company, opts.outpost, "hq outposts jobs register");
|
|
270
861
|
if (definition.schedule.kind === "recurring" &&
|
|
271
862
|
opts.cron !== undefined &&
|
|
272
863
|
definition.schedule.cron !== opts.cron.trim() &&
|
|
273
864
|
!opts.json) {
|
|
274
865
|
console.log(`Using schedule ${definition.schedule.cron} for --cron "${opts.cron.trim()}".`);
|
|
275
866
|
}
|
|
276
|
-
const created = await client.registerCompanyJob(company.uid, definition);
|
|
867
|
+
const created = await client.registerCompanyJob(company.uid, definition, target.outpostId);
|
|
277
868
|
const jobId = created.job?.jobId;
|
|
278
869
|
const revision = created.job?.acceptedRevision;
|
|
279
870
|
if (!jobId || !revision) {
|
|
@@ -281,12 +872,184 @@ async function registerCompanyJobCommand(client, opts) {
|
|
|
281
872
|
process.exit(1);
|
|
282
873
|
}
|
|
283
874
|
if (opts.json) {
|
|
284
|
-
process.stdout.write(JSON.stringify({ jobId, acceptedRevision: revision, company: company.slug }, null, 2) + "\n");
|
|
875
|
+
process.stdout.write(JSON.stringify({ jobId, acceptedRevision: revision, company: company.slug, outpost: target.name }, null, 2) + "\n");
|
|
285
876
|
return;
|
|
286
877
|
}
|
|
287
|
-
console.log(`Registered ${jobId} for ${company.slug}.`);
|
|
878
|
+
console.log(`Registered ${jobId} for ${company.slug} on Outpost ${target.name}.`);
|
|
288
879
|
console.log(`Accepted revision: ${revision}`);
|
|
289
880
|
}
|
|
881
|
+
const COMPANY_JOB_WIRE = {
|
|
882
|
+
edit: "update",
|
|
883
|
+
pause: "disable",
|
|
884
|
+
resume: "enable",
|
|
885
|
+
delete: "delete",
|
|
886
|
+
"run-now": "run-now",
|
|
887
|
+
};
|
|
888
|
+
const COMPANY_JOB_ID = /^job_[A-Za-z0-9]+$/;
|
|
889
|
+
const SAFE_TOKEN = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;
|
|
890
|
+
function safeToken(value) {
|
|
891
|
+
return typeof value === "string" && SAFE_TOKEN.test(value) ? value : undefined;
|
|
892
|
+
}
|
|
893
|
+
function stopCompanyJob(message, stop, code) {
|
|
894
|
+
if (stop.json) {
|
|
895
|
+
process.stdout.write(JSON.stringify({ ok: false, action: stop.action, jobId: stop.jobId, code, message }) + "\n");
|
|
896
|
+
}
|
|
897
|
+
else {
|
|
898
|
+
console.error(message);
|
|
899
|
+
}
|
|
900
|
+
process.exitCode = 1;
|
|
901
|
+
}
|
|
902
|
+
function deleteRerunLine(slug, outpostFlag, jobId, revision) {
|
|
903
|
+
const flag = outpostFlag && SAFE_TOKEN.test(outpostFlag) ? ` --outpost ${outpostFlag}` : "";
|
|
904
|
+
return `hq outposts jobs delete --company ${slug}${flag} ${jobId} --revision ${revision} --yes`;
|
|
905
|
+
}
|
|
906
|
+
function unconfirmedCompanyJob(slug) {
|
|
907
|
+
const status = isCompanySlug(slug)
|
|
908
|
+
? `hq outposts status --company ${slug}`
|
|
909
|
+
: "hq outposts status --company <slug>";
|
|
910
|
+
return `The result could not be confirmed. The server may already have changed the job. Next: ${status}`;
|
|
911
|
+
}
|
|
912
|
+
async function runCompanyJobCommand(client, action, jobId, opts) {
|
|
913
|
+
const stop = { action, jobId, json: Boolean(opts.json) };
|
|
914
|
+
if (!COMPANY_JOB_ID.test(jobId)) {
|
|
915
|
+
stopCompanyJob("That job id is not valid. Next: copy it from hq outposts status --company <slug>.", stop, "invalid_job");
|
|
916
|
+
return;
|
|
917
|
+
}
|
|
918
|
+
const company = await requireCompany(client, opts.company ?? "");
|
|
919
|
+
// Same selector as register: a name or an id. Omitted means the company's default Outpost.
|
|
920
|
+
const target = opts.outpost === undefined
|
|
921
|
+
? undefined
|
|
922
|
+
: await requireCompanyOutpost(client, company, opts.outpost, `hq outposts jobs ${action}`);
|
|
923
|
+
const outpostId = target?.outpostId;
|
|
924
|
+
const listed = await client.listCompanyJobs(company.uid, outpostId);
|
|
925
|
+
const job = listed.find((row) => row.jobId === jobId);
|
|
926
|
+
if (!job) {
|
|
927
|
+
stopCompanyJob(`No job ${jobId} on this Outpost. Next: pass --outpost <id> when the company has more than one, or run hq outposts status --company ${company.slug}.`, stop, "job_not_found");
|
|
928
|
+
return;
|
|
929
|
+
}
|
|
930
|
+
const revision = safeToken(job.acceptedRevision);
|
|
931
|
+
if (!revision) {
|
|
932
|
+
stopCompanyJob("Someone else changed this job. Reload it and try again.", stop, "job_changed");
|
|
933
|
+
return;
|
|
934
|
+
}
|
|
935
|
+
if (opts.revision !== undefined && safeToken(opts.revision) !== revision) {
|
|
936
|
+
stopCompanyJob("Someone else changed this job. Reload it and try again.", stop, "job_changed");
|
|
937
|
+
return;
|
|
938
|
+
}
|
|
939
|
+
if (action === "delete" && !opts.yes) {
|
|
940
|
+
stopCompanyJob(`This will delete job ${jobId} on ${company.slug}'s Outpost.\n` +
|
|
941
|
+
`Accepted revision: ${revision}\n` +
|
|
942
|
+
`Re-run with --yes to confirm: ${deleteRerunLine(company.slug, opts.outpost, jobId, revision)}`, stop, "confirmation_required");
|
|
943
|
+
return;
|
|
944
|
+
}
|
|
945
|
+
if ((action === "edit" || (action === "delete" && opts.yes)) && opts.revision === undefined) {
|
|
946
|
+
stopCompanyJob(action === "delete"
|
|
947
|
+
? `Delete needs --revision ${revision} so a newer change is not removed.\n` +
|
|
948
|
+
`Next: ${deleteRerunLine(company.slug, opts.outpost, jobId, revision)}`
|
|
949
|
+
: `This edit needs the revision you last saw, so it does not overwrite a newer change. Next: re-run with --revision ${revision}.`, stop, "revision_required");
|
|
950
|
+
return;
|
|
951
|
+
}
|
|
952
|
+
if (action === "run-now" && job.status === "paused") {
|
|
953
|
+
stopCompanyJob(`This job is paused. Next: hq outposts jobs resume --company ${company.slug} ${jobId}`, stop, "paused");
|
|
954
|
+
return;
|
|
955
|
+
}
|
|
956
|
+
const definition = action === "edit"
|
|
957
|
+
? mergeCompanyJobEdit(job, {
|
|
958
|
+
prompt: opts.prompt,
|
|
959
|
+
timezone: opts.timezone,
|
|
960
|
+
cron: opts.cron,
|
|
961
|
+
at: opts.at,
|
|
962
|
+
script: opts.script,
|
|
963
|
+
context: opts.context,
|
|
964
|
+
clearScript: opts.clearScript,
|
|
965
|
+
clearContext: opts.clearContext,
|
|
966
|
+
})
|
|
967
|
+
: undefined;
|
|
968
|
+
if (definition?.schedule.kind === "recurring" &&
|
|
969
|
+
opts.cron !== undefined &&
|
|
970
|
+
definition.schedule.cron !== opts.cron.trim() &&
|
|
971
|
+
!opts.json) {
|
|
972
|
+
console.log(`Using schedule ${definition.schedule.cron} for --cron "${opts.cron.trim()}".`);
|
|
973
|
+
}
|
|
974
|
+
const body = await client.companyJobCommand({
|
|
975
|
+
companyUid: company.uid,
|
|
976
|
+
action: COMPANY_JOB_WIRE[action],
|
|
977
|
+
jobId,
|
|
978
|
+
revision,
|
|
979
|
+
...(outpostId ? { outpostId } : {}),
|
|
980
|
+
...(definition ? { definition } : {}),
|
|
981
|
+
});
|
|
982
|
+
printCompanyJobResult(action, company.slug, jobId, body, Boolean(opts.json));
|
|
983
|
+
}
|
|
984
|
+
function printCompanyJobResult(action, slug, jobId, body, json) {
|
|
985
|
+
const stop = { action, jobId, json };
|
|
986
|
+
if (action === "run-now") {
|
|
987
|
+
const runId = safeToken(body.runId);
|
|
988
|
+
const returnedJob = safeToken(body.jobId);
|
|
989
|
+
if (!runId || (returnedJob !== undefined && returnedJob !== jobId)) {
|
|
990
|
+
stopCompanyJob(unconfirmedCompanyJob(slug), stop, "unconfirmed");
|
|
991
|
+
return;
|
|
992
|
+
}
|
|
993
|
+
if (json) {
|
|
994
|
+
process.stdout.write(JSON.stringify({ ok: true, company: slug, action, jobId, runId }) + "\n");
|
|
995
|
+
return;
|
|
996
|
+
}
|
|
997
|
+
console.log(`Started run ${runId} for ${jobId}.`);
|
|
998
|
+
console.log(`Next: hq outposts status --company ${slug}`);
|
|
999
|
+
return;
|
|
1000
|
+
}
|
|
1001
|
+
const job = body.job;
|
|
1002
|
+
const row = job && typeof job === "object" ? job : undefined;
|
|
1003
|
+
const returnedId = safeToken(row?.jobId);
|
|
1004
|
+
const revision = safeToken(row?.acceptedRevision);
|
|
1005
|
+
if (!row || returnedId !== jobId) {
|
|
1006
|
+
stopCompanyJob(unconfirmedCompanyJob(slug), stop, "unconfirmed");
|
|
1007
|
+
return;
|
|
1008
|
+
}
|
|
1009
|
+
if (action === "pause" && row.status !== "paused") {
|
|
1010
|
+
stopCompanyJob(unconfirmedCompanyJob(slug), stop, "unconfirmed");
|
|
1011
|
+
return;
|
|
1012
|
+
}
|
|
1013
|
+
if (action === "resume" && row.status !== "active") {
|
|
1014
|
+
stopCompanyJob(unconfirmedCompanyJob(slug), stop, "unconfirmed");
|
|
1015
|
+
return;
|
|
1016
|
+
}
|
|
1017
|
+
if (action === "delete" && row.deleted !== true) {
|
|
1018
|
+
stopCompanyJob(unconfirmedCompanyJob(slug), stop, "unconfirmed");
|
|
1019
|
+
return;
|
|
1020
|
+
}
|
|
1021
|
+
if (action === "edit" && !revision) {
|
|
1022
|
+
stopCompanyJob(unconfirmedCompanyJob(slug), stop, "unconfirmed");
|
|
1023
|
+
return;
|
|
1024
|
+
}
|
|
1025
|
+
const status = row.status === "paused" || row.status === "active" ? row.status : undefined;
|
|
1026
|
+
if (json) {
|
|
1027
|
+
process.stdout.write(JSON.stringify({
|
|
1028
|
+
ok: true,
|
|
1029
|
+
company: slug,
|
|
1030
|
+
action,
|
|
1031
|
+
jobId,
|
|
1032
|
+
...(revision ? { acceptedRevision: revision } : {}),
|
|
1033
|
+
...(status ? { status } : {}),
|
|
1034
|
+
...(action === "delete" ? { deleted: true } : {}),
|
|
1035
|
+
}) + "\n");
|
|
1036
|
+
return;
|
|
1037
|
+
}
|
|
1038
|
+
if (action === "edit") {
|
|
1039
|
+
console.log(`Updated ${jobId} for ${slug}.`);
|
|
1040
|
+
console.log(`Accepted revision: ${revision}`);
|
|
1041
|
+
return;
|
|
1042
|
+
}
|
|
1043
|
+
if (action === "pause") {
|
|
1044
|
+
console.log(`Paused ${jobId} for ${slug}.`);
|
|
1045
|
+
return;
|
|
1046
|
+
}
|
|
1047
|
+
if (action === "resume") {
|
|
1048
|
+
console.log(`Resumed ${jobId} for ${slug}.`);
|
|
1049
|
+
return;
|
|
1050
|
+
}
|
|
1051
|
+
console.log(`Deleted ${jobId} for ${slug}.`);
|
|
1052
|
+
}
|
|
290
1053
|
function quoteShellArgument(value) {
|
|
291
1054
|
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
292
1055
|
}
|
|
@@ -415,22 +1178,64 @@ export function registerOutpostsCommand(program, deps) {
|
|
|
415
1178
|
.option("--disk <gb>", "Root disk size in GB (EC2 only). Personal boxes only.")
|
|
416
1179
|
.option("--client-ip <ip>", "Your public IP for the box's SSH ingress (optional; server derives it otherwise). Personal boxes only.")
|
|
417
1180
|
.option("--company <slug>", "Provision the company's Outpost instead of your personal box")
|
|
418
|
-
.option("--
|
|
1181
|
+
.option("--name <name>", "With --company: not sent. This server creates the company's main Outpost and does not take a name yet")
|
|
1182
|
+
.option("--yes", "Confirm the monthly charge (required to provision or create)")
|
|
1183
|
+
.option("--json", "With --company, emit one JSON object. Progress goes to stderr.")
|
|
419
1184
|
.action(async function (opts) {
|
|
420
1185
|
try {
|
|
1186
|
+
if (opts.name !== undefined && opts.company === undefined) {
|
|
1187
|
+
console.error("--name names a company Outpost. Next: re-run with --company <slug> --name <name>.");
|
|
1188
|
+
process.exit(1);
|
|
1189
|
+
}
|
|
1190
|
+
if (opts.company !== undefined && opts.name !== undefined) {
|
|
1191
|
+
if (opts.runtime !== undefined || opts.disk !== undefined || opts.clientIp !== undefined) {
|
|
1192
|
+
console.error("Those options are for a personal Outpost. Next: re-run with --company and --name, and leave out --runtime, --disk and --client-ip.");
|
|
1193
|
+
process.exit(1);
|
|
1194
|
+
}
|
|
1195
|
+
try {
|
|
1196
|
+
await createCompanyOutpostCommand(client, host, {
|
|
1197
|
+
company: opts.company,
|
|
1198
|
+
name: opts.name,
|
|
1199
|
+
...(opts.yes !== undefined ? { yes: opts.yes } : {}),
|
|
1200
|
+
...(opts.json ? { json: true } : {}),
|
|
1201
|
+
}, deps.companyProvisionWait);
|
|
1202
|
+
}
|
|
1203
|
+
catch (err) {
|
|
1204
|
+
// The create already reported this and called process.exit.
|
|
1205
|
+
if (err instanceof Error && err.message.startsWith("process.exit("))
|
|
1206
|
+
throw err;
|
|
1207
|
+
if (opts.json) {
|
|
1208
|
+
process.stdout.write(companyProvisionFailureJson(opts.company, err));
|
|
1209
|
+
process.exit(1);
|
|
1210
|
+
}
|
|
1211
|
+
failCompany(opts.company, err, "provision");
|
|
1212
|
+
}
|
|
1213
|
+
return;
|
|
1214
|
+
}
|
|
421
1215
|
if (opts.company !== undefined) {
|
|
422
1216
|
if (opts.runtime !== undefined || opts.disk !== undefined || opts.clientIp !== undefined) {
|
|
423
1217
|
console.error("Those options are for a personal Outpost. Next: re-run with --company and --yes, and leave out --runtime, --disk and --client-ip.");
|
|
424
1218
|
process.exit(1);
|
|
425
1219
|
}
|
|
426
1220
|
try {
|
|
427
|
-
await provisionCompanyOutpost(client, opts.company, opts.yes, deps.companyProvisionWait);
|
|
1221
|
+
await provisionCompanyOutpost(client, opts.company, opts.yes, deps.companyProvisionWait, Boolean(opts.json));
|
|
428
1222
|
}
|
|
429
1223
|
catch (err) {
|
|
1224
|
+
// The charge gate already reported this and called process.exit.
|
|
1225
|
+
if (err instanceof Error && err.message.startsWith("process.exit("))
|
|
1226
|
+
throw err;
|
|
1227
|
+
if (opts.json) {
|
|
1228
|
+
process.stdout.write(companyProvisionFailureJson(opts.company, err));
|
|
1229
|
+
process.exit(1);
|
|
1230
|
+
}
|
|
430
1231
|
failCompany(opts.company, err, "provision");
|
|
431
1232
|
}
|
|
432
1233
|
return;
|
|
433
1234
|
}
|
|
1235
|
+
if (opts.json) {
|
|
1236
|
+
console.error("--json applies to a company Outpost. Next: re-run with --company <slug> --json, or drop --json.");
|
|
1237
|
+
process.exit(1);
|
|
1238
|
+
}
|
|
434
1239
|
// Reject an unrecognized runtime rather than resolving it to claude:
|
|
435
1240
|
// `--runtime codx` would otherwise hand back a silently Claude box.
|
|
436
1241
|
if (opts.runtime !== undefined &&
|
|
@@ -500,9 +1305,19 @@ export function registerOutpostsCommand(program, deps) {
|
|
|
500
1305
|
});
|
|
501
1306
|
outposts
|
|
502
1307
|
.command("list")
|
|
503
|
-
.description("List every Outpost you own")
|
|
1308
|
+
.description("List every Outpost you own, or a company's Outposts with --company")
|
|
1309
|
+
.option("--company <slug>", "List the company's Outposts")
|
|
504
1310
|
.option("--json", "Emit raw JSON")
|
|
505
1311
|
.action(async function (opts) {
|
|
1312
|
+
if (opts.company !== undefined) {
|
|
1313
|
+
try {
|
|
1314
|
+
await listCompanyOutpostsCommand(client, opts.company, Boolean(opts.json));
|
|
1315
|
+
}
|
|
1316
|
+
catch (err) {
|
|
1317
|
+
failCompany(opts.company, err, "status");
|
|
1318
|
+
}
|
|
1319
|
+
return;
|
|
1320
|
+
}
|
|
506
1321
|
try {
|
|
507
1322
|
const rows = await client.list();
|
|
508
1323
|
if (opts.json) {
|
|
@@ -546,8 +1361,10 @@ export function registerOutpostsCommand(program, deps) {
|
|
|
546
1361
|
.description("Show live detail for one Outpost")
|
|
547
1362
|
.option("--id <outpostId>", "Personal Outpost id (defaults to your primary box)")
|
|
548
1363
|
.option("--company <slug>", "Show the company Outpost, its jobs, and each job's run status")
|
|
1364
|
+
.option("--outpost <name|id>", "Which company Outpost, when the company has more than one")
|
|
549
1365
|
.option("--json", "Emit raw JSON")
|
|
550
1366
|
.action(async function (opts) {
|
|
1367
|
+
rejectOutpostWithoutCompany(opts);
|
|
551
1368
|
try {
|
|
552
1369
|
if (opts.company !== undefined) {
|
|
553
1370
|
if (opts.id !== undefined) {
|
|
@@ -555,7 +1372,7 @@ export function registerOutpostsCommand(program, deps) {
|
|
|
555
1372
|
process.exit(1);
|
|
556
1373
|
}
|
|
557
1374
|
try {
|
|
558
|
-
await showCompanyStatus(client, opts.company, Boolean(opts.json));
|
|
1375
|
+
await showCompanyStatus(client, opts.company, Boolean(opts.json), opts.outpost);
|
|
559
1376
|
}
|
|
560
1377
|
catch (err) {
|
|
561
1378
|
failCompany(opts.company, err, "status");
|
|
@@ -575,7 +1392,7 @@ export function registerOutpostsCommand(program, deps) {
|
|
|
575
1392
|
});
|
|
576
1393
|
const jobs = outposts
|
|
577
1394
|
.command("jobs")
|
|
578
|
-
.description("Register a job on a company Outpost");
|
|
1395
|
+
.description("Register, edit, pause, resume, delete, or run a job on a company Outpost");
|
|
579
1396
|
jobs
|
|
580
1397
|
.command("register")
|
|
581
1398
|
.description("Register a company job and print the accepted revision")
|
|
@@ -586,8 +1403,10 @@ export function registerOutpostsCommand(program, deps) {
|
|
|
586
1403
|
.option("--at <iso>", "One-time ISO-8601 instant. Do not combine with --cron")
|
|
587
1404
|
.option("--script <text>", "Optional precondition script stored with the job")
|
|
588
1405
|
.option("--context <json>", "Optional JSON object stored with the job")
|
|
1406
|
+
.option("--outpost <name|id>", "Which company Outpost runs the job, when the company has more than one")
|
|
589
1407
|
.option("--json", "Emit the job id and accepted revision as JSON")
|
|
590
1408
|
.action(async function (opts) {
|
|
1409
|
+
rejectOutpostWithoutCompany(opts);
|
|
591
1410
|
try {
|
|
592
1411
|
await registerCompanyJobCommand(client, opts);
|
|
593
1412
|
}
|
|
@@ -595,6 +1414,45 @@ export function registerOutpostsCommand(program, deps) {
|
|
|
595
1414
|
failCompany(opts.company ?? "", err, "register");
|
|
596
1415
|
}
|
|
597
1416
|
});
|
|
1417
|
+
const companyJobActions = [
|
|
1418
|
+
{ name: "edit", description: "Change a company job. Omitted fields stay as they are. Requires --revision", definition: true, confirm: false },
|
|
1419
|
+
{ name: "pause", description: "Pause a company job", definition: false, confirm: false },
|
|
1420
|
+
{ name: "resume", description: "Resume a paused company job", definition: false, confirm: false },
|
|
1421
|
+
{ name: "delete", description: "Delete a company job (requires --yes)", definition: false, confirm: true },
|
|
1422
|
+
{ name: "run-now", description: "Run a company job once now", definition: false, confirm: false },
|
|
1423
|
+
];
|
|
1424
|
+
for (const spec of companyJobActions) {
|
|
1425
|
+
const command = jobs
|
|
1426
|
+
.command(spec.name)
|
|
1427
|
+
.description(spec.description)
|
|
1428
|
+
.argument("<jobId>", "Job id from hq outposts status --company <slug>")
|
|
1429
|
+
.option("--company <slug>", "Company that owns the Outpost")
|
|
1430
|
+
.option("--outpost <name|id>", "Which company Outpost, when the company has more than one")
|
|
1431
|
+
.option("--revision <rev>", "Accepted revision you last saw. Required for edit and delete --yes");
|
|
1432
|
+
if (spec.definition) {
|
|
1433
|
+
command
|
|
1434
|
+
.option("--prompt <text>", "What the job should do. Omit to keep the current prompt")
|
|
1435
|
+
.option("--timezone <name>", "IANA timezone, such as America/New_York. Omit to keep the current timezone")
|
|
1436
|
+
.option("--cron <expr>", 'Recurring schedule: rate(N minutes|hours|days), a five-field cron with * for day of month and month (such as "*/15 * * * *"; translated to the AWS form), or cron(MIN HOUR ? * DOW *). Omit to keep the current schedule')
|
|
1437
|
+
.option("--at <iso>", "One-time ISO-8601 instant. Do not combine with --cron")
|
|
1438
|
+
.option("--script <text>", "Precondition script stored with the job. Omit to keep the current script")
|
|
1439
|
+
.option("--context <json>", "JSON object stored with the job. Omit to keep the current context")
|
|
1440
|
+
.option("--clear-script", "Remove the job's precondition script")
|
|
1441
|
+
.option("--clear-context", "Remove the job's stored context");
|
|
1442
|
+
}
|
|
1443
|
+
if (spec.confirm) {
|
|
1444
|
+
command.option("--yes", "Confirm deletion of this job");
|
|
1445
|
+
}
|
|
1446
|
+
command.option("--json", "Emit the result as JSON").action(async function (jobId, opts) {
|
|
1447
|
+
rejectOutpostWithoutCompany(opts);
|
|
1448
|
+
try {
|
|
1449
|
+
await runCompanyJobCommand(client, spec.name, jobId, opts);
|
|
1450
|
+
}
|
|
1451
|
+
catch (err) {
|
|
1452
|
+
failCompany(opts.company ?? "", err, spec.name, opts.json ? { action: spec.name, jobId } : undefined);
|
|
1453
|
+
}
|
|
1454
|
+
});
|
|
1455
|
+
}
|
|
598
1456
|
outposts
|
|
599
1457
|
.command("exec <command...>")
|
|
600
1458
|
.description("Run a shell command on an Outpost and print its output (use -- before flags meant for the remote command). " +
|
|
@@ -948,11 +1806,13 @@ export function registerOutpostsCommand(program, deps) {
|
|
|
948
1806
|
.command("refresh-config")
|
|
949
1807
|
.description("Re-apply the server's job runner configuration to the company Outpost (owner or admin)")
|
|
950
1808
|
.requiredOption("--company <slug>", "Company slug")
|
|
1809
|
+
.option("--outpost <name|id>", "Which company Outpost, when the company has more than one")
|
|
951
1810
|
.option("--json", "Emit raw JSON")
|
|
952
1811
|
.action(async function (opts) {
|
|
953
1812
|
try {
|
|
954
1813
|
const company = await requireCompany(client, opts.company);
|
|
955
|
-
await client
|
|
1814
|
+
const target = await requireCompanyOutpost(client, company, opts.outpost, "hq outposts refresh-config");
|
|
1815
|
+
await client.refreshCompanyConfig(company.uid, target.outpostId);
|
|
956
1816
|
// requireCompany validated opts.company; print the user's slug, never server text.
|
|
957
1817
|
const slug = opts.company;
|
|
958
1818
|
if (opts.json) {
|
|
@@ -960,7 +1820,7 @@ export function registerOutpostsCommand(program, deps) {
|
|
|
960
1820
|
return;
|
|
961
1821
|
}
|
|
962
1822
|
console.log(chalk.green(`Configuration refresh started for ${slug}'s Outpost. It takes a few minutes.`));
|
|
963
|
-
console.log(chalk.dim(`Next: hq outposts status --company ${slug}`));
|
|
1823
|
+
console.log(chalk.dim(`Next: hq outposts status --company ${slug}${outpostHint(target, opts.outpost)}`));
|
|
964
1824
|
}
|
|
965
1825
|
catch (err) {
|
|
966
1826
|
failCompany(opts.company, err, "refresh");
|
|
@@ -989,8 +1849,10 @@ export function registerOutpostsCommand(program, deps) {
|
|
|
989
1849
|
.description("Request a fresh login URL for an Outpost")
|
|
990
1850
|
.option("--id <outpostId>", "Outpost id (defaults to your primary box)")
|
|
991
1851
|
.option("--company <slug>", "Request a new sign-in link for the company's Outpost (owner or admin)")
|
|
1852
|
+
.option("--outpost <name|id>", "Which company Outpost, when the company has more than one")
|
|
992
1853
|
.option("--json", "Emit raw JSON")
|
|
993
1854
|
.action(async function (opts) {
|
|
1855
|
+
rejectOutpostWithoutCompany(opts);
|
|
994
1856
|
if (opts.company !== undefined) {
|
|
995
1857
|
if (opts.id) {
|
|
996
1858
|
console.error(chalk.red("Use either --company or --id, not both."));
|
|
@@ -998,13 +1860,14 @@ export function registerOutpostsCommand(program, deps) {
|
|
|
998
1860
|
}
|
|
999
1861
|
try {
|
|
1000
1862
|
const company = await requireCompany(client, opts.company);
|
|
1001
|
-
await client
|
|
1863
|
+
const target = await requireCompanyOutpost(client, company, opts.outpost, "hq outposts login");
|
|
1864
|
+
await client.companySignIn(company.uid, "regenerate", undefined, target.outpostId);
|
|
1002
1865
|
if (opts.json) {
|
|
1003
|
-
process.stdout.write(JSON.stringify({ ok: true, company: company.slug, action: "regenerate" }, null, 2) + "\n");
|
|
1866
|
+
process.stdout.write(JSON.stringify({ ok: true, company: company.slug, outpost: target.name, action: "regenerate" }, null, 2) + "\n");
|
|
1004
1867
|
return;
|
|
1005
1868
|
}
|
|
1006
|
-
console.log(chalk.green(`A new sign-in link was requested for ${company.slug}'s Outpost.`));
|
|
1007
|
-
console.log(chalk.dim(`Next: hq outposts status --company ${company.slug} to read it.`));
|
|
1869
|
+
console.log(chalk.green(`A new sign-in link was requested for ${company.slug}'s Outpost ${target.name}.`));
|
|
1870
|
+
console.log(chalk.dim(`Next: hq outposts status --company ${company.slug}${outpostHint(target, opts.outpost)} to read it.`));
|
|
1008
1871
|
}
|
|
1009
1872
|
catch (err) {
|
|
1010
1873
|
failCompany(opts.company, err, "signin");
|
|
@@ -1034,8 +1897,10 @@ export function registerOutpostsCommand(program, deps) {
|
|
|
1034
1897
|
.description("Submit the Claude sign-in code for an Outpost that is awaiting login")
|
|
1035
1898
|
.option("--id <outpostId>", "Outpost id (defaults to your primary box)")
|
|
1036
1899
|
.option("--company <slug>", "Sign in the company's Outpost (owner or admin)")
|
|
1900
|
+
.option("--outpost <name|id>", "Which company Outpost, when the company has more than one")
|
|
1037
1901
|
.option("--json", "Emit raw JSON")
|
|
1038
1902
|
.action(async function (code, opts) {
|
|
1903
|
+
rejectOutpostWithoutCompany(opts);
|
|
1039
1904
|
const trimmed = code.trim();
|
|
1040
1905
|
if (!trimmed) {
|
|
1041
1906
|
console.error(chalk.red("Provide the sign-in code: hq outposts login-code [--company <slug>] <code>"));
|
|
@@ -1048,13 +1913,14 @@ export function registerOutpostsCommand(program, deps) {
|
|
|
1048
1913
|
}
|
|
1049
1914
|
try {
|
|
1050
1915
|
const company = await requireCompany(client, opts.company);
|
|
1051
|
-
await client
|
|
1916
|
+
const target = await requireCompanyOutpost(client, company, opts.outpost, "hq outposts login-code <code>");
|
|
1917
|
+
await client.companySignIn(company.uid, "code", trimmed, target.outpostId);
|
|
1052
1918
|
if (opts.json) {
|
|
1053
|
-
process.stdout.write(JSON.stringify({ ok: true, company: company.slug, action: "code" }, null, 2) + "\n");
|
|
1919
|
+
process.stdout.write(JSON.stringify({ ok: true, company: company.slug, outpost: target.name, action: "code" }, null, 2) + "\n");
|
|
1054
1920
|
return;
|
|
1055
1921
|
}
|
|
1056
|
-
console.log(chalk.green(`Code submitted. ${company.slug}'s Outpost will finish signing in shortly.`));
|
|
1057
|
-
console.log(chalk.dim(`Next: hq outposts status --company ${company.slug}`));
|
|
1922
|
+
console.log(chalk.green(`Code submitted. ${company.slug}'s Outpost ${target.name} will finish signing in shortly.`));
|
|
1923
|
+
console.log(chalk.dim(`Next: hq outposts status --company ${company.slug}${outpostHint(target, opts.outpost)}`));
|
|
1058
1924
|
}
|
|
1059
1925
|
catch (err) {
|
|
1060
1926
|
failCompany(opts.company, err, "signin");
|
|
@@ -1080,8 +1946,24 @@ export function registerOutpostsCommand(program, deps) {
|
|
|
1080
1946
|
.command("destroy")
|
|
1081
1947
|
.description("Tear down (permanently destroy) an Outpost")
|
|
1082
1948
|
.option("--id <outpostId>", "Outpost id (defaults to your primary box)")
|
|
1949
|
+
.option("--company <slug>", "Destroy one of the company's Outposts (owner or admin)")
|
|
1950
|
+
.option("--outpost <name|id>", "Which company Outpost, when the company has more than one")
|
|
1951
|
+
.option("--json", "With --company and --yes: print one result object on stdout")
|
|
1083
1952
|
.option("--yes", "Confirm the irreversible teardown (required)")
|
|
1084
1953
|
.action(async function (opts) {
|
|
1954
|
+
rejectOutpostWithoutCompany(opts);
|
|
1955
|
+
if (opts.company !== undefined) {
|
|
1956
|
+
if (opts.id) {
|
|
1957
|
+
console.error(chalk.red("Use either --company or --id, not both."));
|
|
1958
|
+
process.exit(1);
|
|
1959
|
+
}
|
|
1960
|
+
await destroyCompanyOutpostCommand(client, host, { company: opts.company, outpost: opts.outpost, yes: opts.yes, json: opts.json }, deps.companyDestroyWait);
|
|
1961
|
+
return;
|
|
1962
|
+
}
|
|
1963
|
+
if (opts.json) {
|
|
1964
|
+
console.error("--json applies to a company Outpost. Next: re-run with --company <slug> --yes --json, or drop --json.");
|
|
1965
|
+
process.exit(1);
|
|
1966
|
+
}
|
|
1085
1967
|
const target = opts.id ?? "your primary Outpost";
|
|
1086
1968
|
if (!opts.yes) {
|
|
1087
1969
|
console.error(chalk.yellow(`This will permanently destroy ${target} and delete its cloud resources. ` +
|