@withandeo/cli 0.19.7 → 0.19.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +44 -0
- package/dist/api.d.ts +66 -0
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +37 -0
- package/dist/api.js.map +1 -1
- package/dist/artifact.d.ts.map +1 -1
- package/dist/artifact.js +26 -0
- package/dist/artifact.js.map +1 -1
- package/dist/capability-preflight.d.ts.map +1 -1
- package/dist/capability-preflight.js +19 -0
- package/dist/capability-preflight.js.map +1 -1
- package/dist/contracts.d.ts +91 -0
- package/dist/contracts.d.ts.map +1 -1
- package/dist/documentation.d.ts +1 -1
- package/dist/documentation.d.ts.map +1 -1
- package/dist/documentation.js +1 -1
- package/dist/documentation.js.map +1 -1
- package/dist/flagship-contract.d.ts +63 -0
- package/dist/flagship-contract.d.ts.map +1 -0
- package/dist/flagship-contract.js +117 -0
- package/dist/flagship-contract.js.map +1 -0
- package/dist/flagship-package.d.ts +3 -0
- package/dist/flagship-package.d.ts.map +1 -0
- package/dist/flagship-package.js +31 -0
- package/dist/flagship-package.js.map +1 -0
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +262 -3
- package/dist/main.js.map +1 -1
- package/dist/workflow-dev.d.ts.map +1 -1
- package/dist/workflow-dev.js +2 -0
- package/dist/workflow-dev.js.map +1 -1
- package/dist/workflow-package.d.ts.map +1 -1
- package/dist/workflow-package.js +3 -0
- package/dist/workflow-package.js.map +1 -1
- package/package.json +1 -1
- package/skill/tender-accounts/SKILL.md +6 -0
- package/skill/tender-accounts/references/feature-flags.md +90 -0
- package/skill/tender-accounts/references/managed-source.md +81 -0
package/dist/main.js
CHANGED
|
@@ -20,6 +20,7 @@ import { initializeStarter } from "./starter.js";
|
|
|
20
20
|
import { normalizeAndeoEnvironment } from "./andeo-environment.js";
|
|
21
21
|
import { DOC_TOPICS, readDocumentation, installDocumentation, installedSkillCheck } from "./documentation.js";
|
|
22
22
|
import { capabilityPreflight } from "./capability-preflight.js";
|
|
23
|
+
import { flagEnvironment, flagKey, rolloutPercentage, customerIds } from "./flagship-contract.js";
|
|
23
24
|
const packageMetadata = createRequire(import.meta.url)("../package.json");
|
|
24
25
|
if (typeof packageMetadata.version !== "string" || packageMetadata.version.length === 0) {
|
|
25
26
|
throw new Error("Andeo CLI package version is missing.");
|
|
@@ -101,6 +102,8 @@ async function execute(topic, options, cwd, runtime) {
|
|
|
101
102
|
return { value: await workflowCommand(topic.slice(10), cwd, options, runtime) };
|
|
102
103
|
if (topic.startsWith("email "))
|
|
103
104
|
return { value: await emailCommand(topic.slice(6), cwd, options, runtime) };
|
|
105
|
+
if (topic.startsWith("flagship "))
|
|
106
|
+
return { value: await flagshipCommand(topic.slice(9), cwd, options, runtime) };
|
|
104
107
|
switch (topic) {
|
|
105
108
|
case "docs": {
|
|
106
109
|
const name = options.positionals[0];
|
|
@@ -223,6 +226,65 @@ async function execute(topic, options, cwd, runtime) {
|
|
|
223
226
|
return { value: await sourceOperationRetry(cwd, options, runtime) };
|
|
224
227
|
case "source change create":
|
|
225
228
|
return { value: await sourceChangeCreate(cwd, options, runtime) };
|
|
229
|
+
case "source merge": {
|
|
230
|
+
const value = await sourceMerge(cwd, options, runtime);
|
|
231
|
+
return { value, exitCode: value.status === "timeout" ? 3 : value.merge && ["awaiting_approval", "needs_attention", "superseded", "authorization_required", "reconciliation_required", "cancelled"].includes(value.merge.stage) ? 2 : 0 };
|
|
232
|
+
}
|
|
233
|
+
case "source merge status":
|
|
234
|
+
case "source merge resume":
|
|
235
|
+
case "source merge cancel":
|
|
236
|
+
case "source merge take-over": {
|
|
237
|
+
const mergeId = requiredFlag(options, "merge");
|
|
238
|
+
if (!/^smi_[A-Za-z0-9_-]+$/.test(mergeId))
|
|
239
|
+
throw new CliError("argument_invalid", "--merge must be a merge intent ID (smi_...).");
|
|
240
|
+
const key = topic === "source merge take-over" ? requiredFlag(options, "idempotency-key") : undefined;
|
|
241
|
+
if (key && !/^[A-Za-z0-9:_-]{8,128}$/.test(key))
|
|
242
|
+
throw new CliError("argument_invalid", "--idempotency-key must contain 8–128 letters, numbers, colons, underscores or hyphens.");
|
|
243
|
+
const access = await sourceReadAccess(cwd, options, runtime);
|
|
244
|
+
const current = await access.client.sourceMergeStatus(mergeId);
|
|
245
|
+
requireLinkedSourceChange(access.projectId, current.merge.projectId);
|
|
246
|
+
if (current.merge.id !== mergeId || current.merge.repositoryId !== access.repository.id)
|
|
247
|
+
throw new CliError("source_merge_changed", "The returned merge does not match this repository and intent.");
|
|
248
|
+
if (topic === "source merge status")
|
|
249
|
+
return { value: current };
|
|
250
|
+
return { value: topic === "source merge resume" ? await access.client.resumeSourceMergeById(mergeId)
|
|
251
|
+
: key ? await access.client.takeOverSourceMergeById(mergeId, key) : await access.client.cancelSourceMergeById(mergeId) };
|
|
252
|
+
}
|
|
253
|
+
case "source change resume": {
|
|
254
|
+
const access = await sourceReadAccess(cwd, options, runtime);
|
|
255
|
+
const changeId = requiredSourceChangeRequestId(requiredFlag(options, "change"));
|
|
256
|
+
const current = await access.client.sourceChangeRequest(changeId);
|
|
257
|
+
requireLinkedSourceChange(access.projectId, current.changeRequest.projectId);
|
|
258
|
+
return { value: await access.client.resumeSourceMerge(changeId) };
|
|
259
|
+
}
|
|
260
|
+
case "source change cancel":
|
|
261
|
+
case "source change take-over": {
|
|
262
|
+
const changeId = requiredSourceChangeRequestId(requiredFlag(options, "change"));
|
|
263
|
+
const key = topic === "source change take-over" ? requiredFlag(options, "idempotency-key") : null;
|
|
264
|
+
const access = await sourceReadAccess(cwd, options, runtime);
|
|
265
|
+
const current = await access.client.sourceChangeRequest(changeId);
|
|
266
|
+
requireLinkedSourceChange(access.projectId, current.changeRequest.projectId);
|
|
267
|
+
return { value: key ? await access.client.takeOverSourceMerge(changeId, key) : await access.client.cancelSourceMerge(changeId) };
|
|
268
|
+
}
|
|
269
|
+
case "source merge-policy show":
|
|
270
|
+
case "source merge-policy set": {
|
|
271
|
+
const access = await sourceReadAccess(cwd, options, runtime);
|
|
272
|
+
if (topic === "source merge-policy show")
|
|
273
|
+
return { value: await access.client.sourceMergePolicy(access.repository.id) };
|
|
274
|
+
const mode = requiredFlag(options, "mode");
|
|
275
|
+
if (!["human-review", "agent", "administrator-authorized"].includes(mode))
|
|
276
|
+
throw new CliError("source_merge_policy_invalid", "--mode must be human-review, agent or administrator-authorized.");
|
|
277
|
+
const principalIds = (options.flags.get("principals") ?? "").split(",").filter(Boolean);
|
|
278
|
+
if (mode === "administrator-authorized" && principalIds.length)
|
|
279
|
+
throw new CliError("source_merge_policy_invalid", "Administrator-authorized mode requires exact per-operation approval; omit --principals.");
|
|
280
|
+
const executionSeconds = options.flags.has("execution-seconds") ? Number(options.flags.get("execution-seconds")) : undefined;
|
|
281
|
+
if (executionSeconds !== undefined && (!Number.isSafeInteger(executionSeconds) || executionSeconds < 300 || executionSeconds > 86400))
|
|
282
|
+
throw new CliError("argument_invalid", "--execution-seconds must be between 300 and 86400.");
|
|
283
|
+
return { value: await access.client.sourceMergePolicy(access.repository.id, { enabled: mode === "agent",
|
|
284
|
+
principalIds, validationMode: mode === "administrator-authorized" ? "administrator_authorized" : "native_preview",
|
|
285
|
+
buildAdmission: mode === "administrator-authorized" ? "source_only" : "native",
|
|
286
|
+
expectedRevision: Number(requiredFlag(options, "expected-revision")), idempotencyKey: requiredFlag(options, "idempotency-key"), executionSeconds }) };
|
|
287
|
+
}
|
|
226
288
|
case "source change list":
|
|
227
289
|
return { value: await sourceChangeList(cwd, options, runtime) };
|
|
228
290
|
case "source change show":
|
|
@@ -1271,6 +1333,86 @@ async function submitProtectedSourcePush(gitRoot, projectId, repository, branch,
|
|
|
1271
1333
|
await rm(temporary, { recursive: true, force: true });
|
|
1272
1334
|
}
|
|
1273
1335
|
}
|
|
1336
|
+
async function sourceMerge(cwd, options, runtime) {
|
|
1337
|
+
if (options.booleans.has("wait") && options.booleans.has("no-wait"))
|
|
1338
|
+
throw new CliError("argument_invalid", "Choose --wait or --no-wait.");
|
|
1339
|
+
const changeId = options.flags.get("change");
|
|
1340
|
+
if (changeId && (options.flags.has("head") || options.flags.has("title")))
|
|
1341
|
+
throw new CliError("argument_invalid", "Choose --change or --head with --title.");
|
|
1342
|
+
const idempotencyKey = requiredFlag(options, "idempotency-key");
|
|
1343
|
+
if (!/^[A-Za-z0-9:_-]{8,128}$/.test(idempotencyKey))
|
|
1344
|
+
throw new CliError("argument_invalid", "--idempotency-key must contain 8–128 letters, numbers, colons, underscores or hyphens.");
|
|
1345
|
+
const timeout = Number(options.flags.get("timeout-seconds") ?? "1200");
|
|
1346
|
+
if (!Number.isSafeInteger(timeout) || timeout < 1 || timeout > 3600)
|
|
1347
|
+
throw new CliError("argument_invalid", "--timeout-seconds must be between 1 and 3600.");
|
|
1348
|
+
const headBranch = changeId ? undefined : requiredFlag(options, "head");
|
|
1349
|
+
if (headBranch)
|
|
1350
|
+
assertGitBranchName(headBranch);
|
|
1351
|
+
const title = changeId ? undefined : requiredFlag(options, "title");
|
|
1352
|
+
if (changeId)
|
|
1353
|
+
requiredSourceChangeRequestId(changeId);
|
|
1354
|
+
const expectedHead = options.flags.get("expected-head"), expectedSnapshot = options.flags.get("expected-snapshot");
|
|
1355
|
+
if (expectedHead && !/^[a-f0-9]{40}$/.test(expectedHead))
|
|
1356
|
+
throw new CliError("argument_invalid", "--expected-head must be a commit SHA.");
|
|
1357
|
+
if (expectedSnapshot && (!changeId || !Number.isSafeInteger(Number(expectedSnapshot)) || Number(expectedSnapshot) < 1))
|
|
1358
|
+
throw new CliError("argument_invalid", "--expected-snapshot requires --change and a positive integer.");
|
|
1359
|
+
const expectedBase = options.flags.get("expected-base"), policyRevision = options.flags.get("expected-policy-revision");
|
|
1360
|
+
const validationMode = options.flags.get("validation-mode"), continuation = options.flags.get("continuation");
|
|
1361
|
+
const authorize = options.booleans.has("authorize"), dryRun = options.booleans.has("dry-run"), note = options.flags.get("note");
|
|
1362
|
+
if (expectedBase && !/^[a-f0-9]{40}$/.test(expectedBase))
|
|
1363
|
+
throw new CliError("argument_invalid", "--expected-base must be a commit SHA.");
|
|
1364
|
+
if (policyRevision !== undefined && (!/^\d+$/.test(policyRevision) || !Number.isSafeInteger(Number(policyRevision))))
|
|
1365
|
+
throw new CliError("argument_invalid", "--expected-policy-revision must be a nonnegative integer.");
|
|
1366
|
+
if (validationMode !== undefined && validationMode !== "native_preview" && validationMode !== "administrator_authorized")
|
|
1367
|
+
throw new CliError("argument_invalid", "--validation-mode must be native_preview or administrator_authorized.");
|
|
1368
|
+
if (continuation !== undefined && continuation !== "source_only")
|
|
1369
|
+
throw new CliError("argument_invalid", "--continuation must be source_only.");
|
|
1370
|
+
if (note !== undefined && (note.length < 1 || note.length > 1000))
|
|
1371
|
+
throw new CliError("argument_invalid", "--note must contain 1–1000 characters.");
|
|
1372
|
+
if (validationMode === "administrator_authorized") {
|
|
1373
|
+
if (changeId || expectedSnapshot)
|
|
1374
|
+
throw new CliError("argument_invalid", "Administrator-authorized merges use --head and --title without a native change request.");
|
|
1375
|
+
if (!dryRun && (!authorize || !expectedBase || !expectedHead || policyRevision === undefined))
|
|
1376
|
+
throw new CliError("argument_invalid", "Administrator-authorized merges require --authorize, --expected-base, --expected-head and --expected-policy-revision. Use --dry-run to inspect the exact snapshot first.");
|
|
1377
|
+
}
|
|
1378
|
+
else if (authorize || expectedBase !== undefined || policyRevision !== undefined || continuation !== undefined || note !== undefined) {
|
|
1379
|
+
throw new CliError("argument_invalid", "Administrator authorization flags require --validation-mode administrator_authorized.");
|
|
1380
|
+
}
|
|
1381
|
+
const access = await sourceReadAccess(cwd, options, runtime);
|
|
1382
|
+
const result = await access.client.sourceMerge(access.repository.id, { headBranch, title, changeId, idempotencyKey,
|
|
1383
|
+
expectedHead, expectedSnapshot: expectedSnapshot ? Number(expectedSnapshot) : undefined, dryRun,
|
|
1384
|
+
expectedBase, expectedPolicyRevision: policyRevision === undefined ? undefined : Number(policyRevision),
|
|
1385
|
+
validationMode, authorize: validationMode === "administrator_authorized" ? authorize : undefined,
|
|
1386
|
+
continuation: validationMode === "administrator_authorized" ? "source_only" : undefined, note });
|
|
1387
|
+
if (result.merge) {
|
|
1388
|
+
requireLinkedSourceChange(access.projectId, result.merge.projectId);
|
|
1389
|
+
if (result.merge.repositoryId !== access.repository.id)
|
|
1390
|
+
throw new CliError("source_merge_changed", "The returned merge belongs to another repository.");
|
|
1391
|
+
}
|
|
1392
|
+
if (!result.merge || !options.booleans.has("wait"))
|
|
1393
|
+
return result;
|
|
1394
|
+
let merge = result.merge;
|
|
1395
|
+
for (let poll = 0; poll < Math.ceil(timeout / 5); poll++) {
|
|
1396
|
+
if (merge.terminal || ["merged", "deployed", "needs_attention", "publication_not_enabled", "superseded", "authorization_required", "reconciliation_required", "cancelled"].includes(merge.stage))
|
|
1397
|
+
return { ...result, merge };
|
|
1398
|
+
if (merge.stage === "awaiting_approval")
|
|
1399
|
+
return { ...result, merge };
|
|
1400
|
+
await runtime.sleep(Math.min(5000, timeout * 1000 - poll * 5000));
|
|
1401
|
+
const current = merge.changeId ? await access.client.sourceChangeRequest(merge.changeId) : await access.client.sourceMergeStatus(merge.id);
|
|
1402
|
+
if ("changeRequest" in current)
|
|
1403
|
+
requireLinkedSourceChange(access.projectId, current.changeRequest.projectId);
|
|
1404
|
+
if (!current.merge || current.merge.id !== merge.id)
|
|
1405
|
+
throw new CliError("source_merge_changed", "The merge intent changed. Inspect this exact change before continuing.");
|
|
1406
|
+
requireLinkedSourceChange(access.projectId, current.merge.projectId);
|
|
1407
|
+
if (current.merge.repositoryId !== access.repository.id)
|
|
1408
|
+
throw new CliError("source_merge_changed", "The returned merge belongs to another repository.");
|
|
1409
|
+
merge = current.merge;
|
|
1410
|
+
}
|
|
1411
|
+
if (merge.terminal || ["merged", "deployed", "awaiting_approval", "needs_attention", "publication_not_enabled", "superseded", "authorization_required", "reconciliation_required", "cancelled"].includes(merge.stage))
|
|
1412
|
+
return { ...result, merge };
|
|
1413
|
+
return { ...result, status: "timeout", merge: { ...merge, waitTimedOut: true }, next: merge.changeId
|
|
1414
|
+
? `andeo source change show --change ${merge.changeId} --json` : `andeo source merge status --merge ${merge.id} --json` };
|
|
1415
|
+
}
|
|
1274
1416
|
async function sourceChangeCreate(cwd, options, runtime) {
|
|
1275
1417
|
const access = await sourceReadAccess(cwd, options, runtime);
|
|
1276
1418
|
if (access.repository.changeRequestProtection !== "enabled") {
|
|
@@ -2324,7 +2466,82 @@ function exampleConfig() {
|
|
|
2324
2466
|
returnPath: "/account",
|
|
2325
2467
|
};
|
|
2326
2468
|
}
|
|
2469
|
+
async function flagshipCommand(command, cwd, options, runtime) {
|
|
2470
|
+
let environment;
|
|
2471
|
+
try {
|
|
2472
|
+
environment = flagEnvironment(requiredFlag(options, "env"));
|
|
2473
|
+
}
|
|
2474
|
+
catch {
|
|
2475
|
+
throw new CliError("flagship_environment_invalid", "Use --env development, preview, or production explicitly.");
|
|
2476
|
+
}
|
|
2477
|
+
const context = await optionalContext(cwd);
|
|
2478
|
+
const projectId = options.flags.get("project") ?? context?.link?.projectId;
|
|
2479
|
+
if (!projectId)
|
|
2480
|
+
throw new CliError("project_required", "Link a project or pass --project.");
|
|
2481
|
+
const { client } = await authenticatedClient(cwd, context?.link ?? null, options, runtime);
|
|
2482
|
+
if (command.startsWith("apps "))
|
|
2483
|
+
return client.flagship(projectId, ["apps", ...(command === "apps ensure" ? ["ensure"] : [])], environment, command === "apps ensure" ? { body: {} } : {});
|
|
2484
|
+
const action = command.slice(6), app = options.positionals[0], key = options.positionals[1];
|
|
2485
|
+
const segments = ["flags", app, ...(key ? [flagKey(key)] : [])];
|
|
2486
|
+
if (action === "list" || action === "get")
|
|
2487
|
+
return client.flagship(projectId, segments, environment);
|
|
2488
|
+
segments.push(action);
|
|
2489
|
+
if (action === "changelog")
|
|
2490
|
+
return client.flagship(projectId, segments, environment);
|
|
2491
|
+
if (action === "evaluate") {
|
|
2492
|
+
const context = requiredFlag(options, "context");
|
|
2493
|
+
if (!context.startsWith("customerId="))
|
|
2494
|
+
throw new CliError("flagship_context_invalid", "Use --context customerId=IDENTIFIER.");
|
|
2495
|
+
const [customerId] = customerIds([context.slice(11)]);
|
|
2496
|
+
return client.flagship(projectId, segments, environment, { customerId });
|
|
2497
|
+
}
|
|
2498
|
+
const revision = options.flags.get("expected-revision");
|
|
2499
|
+
// Read before mutation by default. Server rechecks this revision inside serialization.
|
|
2500
|
+
let expectedRevision;
|
|
2501
|
+
if (revision !== undefined) {
|
|
2502
|
+
expectedRevision = Number(revision);
|
|
2503
|
+
if (!/^\d+$/.test(revision) || !Number.isSafeInteger(expectedRevision))
|
|
2504
|
+
throw new CliError("flagship_revision_invalid", "Expected revision must be a nonnegative integer.");
|
|
2505
|
+
}
|
|
2506
|
+
else if (action === "create")
|
|
2507
|
+
expectedRevision = 0;
|
|
2508
|
+
else
|
|
2509
|
+
expectedRevision = (await client.flagship(projectId, segments.slice(0, -1), environment)).revision;
|
|
2510
|
+
const body = { expectedRevision };
|
|
2511
|
+
if (action === "audience") {
|
|
2512
|
+
const all = options.booleans.has("all"), file = options.flags.get("customer-ids-file");
|
|
2513
|
+
if (all === !!file)
|
|
2514
|
+
throw new CliError("flagship_audience_invalid", "Choose --all or --customer-ids-file.");
|
|
2515
|
+
if (all)
|
|
2516
|
+
body.all = true;
|
|
2517
|
+
else {
|
|
2518
|
+
let value;
|
|
2519
|
+
try {
|
|
2520
|
+
value = JSON.parse(new TextDecoder().decode(await readBoundedFile(resolve(cwd, file), 192 * 1024)));
|
|
2521
|
+
}
|
|
2522
|
+
catch {
|
|
2523
|
+
throw new CliError("flagship_audience_invalid", "The customer ID file must contain a bounded JSON array.");
|
|
2524
|
+
}
|
|
2525
|
+
body.customerIds = customerIds(value);
|
|
2526
|
+
}
|
|
2527
|
+
}
|
|
2528
|
+
if (action === "rollout") {
|
|
2529
|
+
if (requiredFlag(options, "to") !== "on" || requiredFlag(options, "by") !== "customerId")
|
|
2530
|
+
throw new CliError("flagship_rollout_invalid", "V1 supports --to on --by customerId.");
|
|
2531
|
+
const percentage = requiredFlag(options, "percentage");
|
|
2532
|
+
if (!/^(?:\d+)(?:\.\d{1,2})?$/.test(percentage))
|
|
2533
|
+
throw new CliError("flagship_percentage_invalid", "Use a percentage from 0 to 100 with at most two decimals.");
|
|
2534
|
+
body.percentage = rolloutPercentage(Number(percentage));
|
|
2535
|
+
}
|
|
2536
|
+
return client.flagship(projectId, segments, environment, { body });
|
|
2537
|
+
}
|
|
2327
2538
|
function commandTopic(argv) {
|
|
2539
|
+
if (argv[0] === "flagship") {
|
|
2540
|
+
const topic = argv.slice(0, 3).join(" ");
|
|
2541
|
+
if (argv.length < 3 || argv.includes("--help") || argv.includes("-h"))
|
|
2542
|
+
return { topic: "help", values: ["flagship"] };
|
|
2543
|
+
return { topic, values: argv.slice(3) };
|
|
2544
|
+
}
|
|
2328
2545
|
if (argv.length === 0 || argv[0] === "help" || argv[0] === "--help" || argv[0] === "-h") {
|
|
2329
2546
|
return { topic: "help", values: argv.slice(1) };
|
|
2330
2547
|
}
|
|
@@ -2339,7 +2556,11 @@ function commandTopic(argv) {
|
|
|
2339
2556
|
const child = argv[1];
|
|
2340
2557
|
if (!child || child.startsWith("--"))
|
|
2341
2558
|
return { topic: "help", values: [parent] };
|
|
2342
|
-
if (parent === "source" &&
|
|
2559
|
+
if (parent === "source" && child === "merge" && argv[2] && !argv[2].startsWith("-")) {
|
|
2560
|
+
const topic = `source merge ${argv[2]}`;
|
|
2561
|
+
return argv.includes("--help") || argv.includes("-h") ? { topic: "help", values: [topic] } : { topic, values: argv.slice(3) };
|
|
2562
|
+
}
|
|
2563
|
+
if (parent === "source" && (child === "default" || child === "history" || child === "operation" || child === "change" || child === "policy" || child === "merge-policy" || child === "reconnect" || child === "branch")) {
|
|
2343
2564
|
const grandchild = argv[2];
|
|
2344
2565
|
if (!grandchild || grandchild.startsWith("--"))
|
|
2345
2566
|
return { topic: "help", values: [`${parent} ${child}`] };
|
|
@@ -2374,7 +2595,7 @@ function parseOptions(values) {
|
|
|
2374
2595
|
const booleans = new Set();
|
|
2375
2596
|
const positionals = [];
|
|
2376
2597
|
const booleanNames = new Set([
|
|
2377
|
-
"json", "token-stdin", "dry-run", "no-wait", "force", "reauthorize",
|
|
2598
|
+
"json", "token-stdin", "dry-run", "no-wait", "force", "reauthorize", "authorize",
|
|
2378
2599
|
"device", "wait", "no-open", "local", "all", "production",
|
|
2379
2600
|
]);
|
|
2380
2601
|
for (let index = 0; index < values.length; index += 1) {
|
|
@@ -2404,6 +2625,20 @@ function parseOptions(values) {
|
|
|
2404
2625
|
return { flags, booleans, positionals, json: booleans.has("json") };
|
|
2405
2626
|
}
|
|
2406
2627
|
function assertAllowedOptions(topic, options) {
|
|
2628
|
+
if (topic.startsWith("flagship ")) {
|
|
2629
|
+
const common = ["cwd", "json", "api-url", "token-stdin", "profile", "project", "env"];
|
|
2630
|
+
const extras = { "flags create": ["expected-revision"], "flags enable": ["expected-revision"], "flags disable": ["expected-revision"],
|
|
2631
|
+
"flags audience": ["expected-revision", "customer-ids-file", "all"], "flags rollout": ["expected-revision", "to", "percentage", "by"], "flags evaluate": ["context"], "flags reconcile": ["expected-revision"] };
|
|
2632
|
+
const sub = topic.slice(9);
|
|
2633
|
+
if (!["apps ensure", "apps list", "flags list", "flags get", "flags create", "flags enable", "flags disable", "flags audience", "flags rollout", "flags evaluate", "flags changelog", "flags reconcile"].includes(sub))
|
|
2634
|
+
throw new CliError("argument_unknown", "Unknown flagship command.");
|
|
2635
|
+
if ([...options.flags.keys(), ...options.booleans].some(k => ![...common, ...(extras[sub] ?? [])].includes(k)))
|
|
2636
|
+
throw new CliError("argument_unknown", "Unsupported flagship option.");
|
|
2637
|
+
const length = sub.startsWith("apps") ? 0 : sub === "flags list" ? 1 : 2;
|
|
2638
|
+
if (options.positionals.length !== length)
|
|
2639
|
+
throw new CliError("argument_required", `Expected ${length} positional arguments for ${topic}.`);
|
|
2640
|
+
return;
|
|
2641
|
+
}
|
|
2407
2642
|
const common = ["cwd", "json"];
|
|
2408
2643
|
const authentication = ["api-url", "token-stdin", "profile"];
|
|
2409
2644
|
const allowed = {
|
|
@@ -2502,6 +2737,17 @@ function assertAllowedOptions(topic, options) {
|
|
|
2502
2737
|
"source operation status": [...common, ...authentication, "operation"],
|
|
2503
2738
|
"source operation retry": [...common, ...authentication, "operation", "no-wait", "timeout-seconds"],
|
|
2504
2739
|
"source change create": [...common, ...authentication, "project", "title", "description", "base", "head"],
|
|
2740
|
+
"source merge": [...common, ...authentication, "project", "head", "title", "change", "expected-head", "expected-snapshot", "idempotency-key", "dry-run", "wait", "no-wait", "timeout-seconds",
|
|
2741
|
+
"expected-base", "expected-policy-revision", "validation-mode", "authorize", "continuation", "note"],
|
|
2742
|
+
"source merge status": [...common, ...authentication, "project", "merge"],
|
|
2743
|
+
"source merge resume": [...common, ...authentication, "project", "merge"],
|
|
2744
|
+
"source merge cancel": [...common, ...authentication, "project", "merge"],
|
|
2745
|
+
"source merge take-over": [...common, ...authentication, "project", "merge", "idempotency-key"],
|
|
2746
|
+
"source change resume": [...common, ...authentication, "project", "change"],
|
|
2747
|
+
"source change cancel": [...common, ...authentication, "project", "change"],
|
|
2748
|
+
"source change take-over": [...common, ...authentication, "project", "change", "idempotency-key"],
|
|
2749
|
+
"source merge-policy show": [...common, ...authentication, "project"],
|
|
2750
|
+
"source merge-policy set": [...common, ...authentication, "project", "mode", "principals", "expected-revision", "idempotency-key", "execution-seconds"],
|
|
2505
2751
|
"source change list": [...common, ...authentication, "project", "state"],
|
|
2506
2752
|
"source change show": [...common, ...authentication, "change"],
|
|
2507
2753
|
"source change close": [...common, ...authentication, "change"],
|
|
@@ -2634,6 +2880,8 @@ function writeOutput(value, options, runtime) {
|
|
|
2634
2880
|
runtime.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
|
|
2635
2881
|
}
|
|
2636
2882
|
function helpText(topic) {
|
|
2883
|
+
if (topic === "flagship")
|
|
2884
|
+
return "Usage: andeo flagship apps ensure|list --env development|preview|production [--project ID] [--json]\n andeo flagship flags list APP_ID --env ENV\n andeo flagship flags create|get|enable|disable|changelog|reconcile APP_ID KEY --env ENV [--expected-revision N]\n andeo flagship flags audience APP_ID KEY --env ENV --customer-ids-file FILE|--all\n andeo flagship flags rollout APP_ID KEY --env ENV --to on --percentage 5 --by customerId\n andeo flagship flags evaluate APP_ID KEY --env ENV --context customerId=synthetic-customer-a\n\nDefaults off. Audience and percentage edits preserve each other and do not enable a disabled flag. Changes propagate; pending writes must be reconciled.";
|
|
2637
2885
|
if (topic === "email" || topic?.startsWith("email "))
|
|
2638
2886
|
return `Usage: andeo email <capabilities|create|list|cursor|wait|read|messages|retire> [options]
|
|
2639
2887
|
|
|
@@ -2710,7 +2958,18 @@ Account workflow entitlement is required for new starts. Run status is authorita
|
|
|
2710
2958
|
"source operation": "Usage: andeo source operation <status|retry> --operation sop_... [--json]",
|
|
2711
2959
|
"source operation status": "Usage: andeo source operation status --operation sop_... [--json]\n\nRead durable status and safe evidence for one exact source operation.",
|
|
2712
2960
|
"source operation retry": "Usage: andeo source operation retry --operation sop_... [--no-wait] [--timeout-seconds N] [--json]\n\nRetry or resume the same durable source operation without changing its reviewed intent. This never publishes production.",
|
|
2713
|
-
"source change": "Usage: andeo source change <create|list|show|close> [options]\n\nCreate and inspect exact preview-backed changes
|
|
2961
|
+
"source change": "Usage: andeo source change <create|list|show|close|resume|cancel|take-over> [options]\n\nCreate and inspect exact preview-backed changes, or recover an accepted merge intent.",
|
|
2962
|
+
"source change cancel": "Usage: andeo source change cancel --change scr_... [--json]\n\nWithdraw new work for an accepted intent. Already-issued effects require readback; source may already be merged.",
|
|
2963
|
+
"source change take-over": "Usage: andeo source change take-over --change scr_... --idempotency-key KEY [--json]\n\nAdministrator only. Records a new bounded authorization for the same exact inputs; then use source change resume.",
|
|
2964
|
+
"source merge": "Usage: andeo source merge --head BRANCH --title TITLE --idempotency-key KEY [--dry-run] [--wait] [--timeout-seconds N] [--json]\n andeo source merge --change scr_... --idempotency-key KEY [--wait] [--json]\n andeo source merge --head BRANCH --title TITLE --validation-mode administrator_authorized --expected-base SHA --expected-head SHA --expected-policy-revision N --authorize --idempotency-key KEY [--continuation source_only] [--note TEXT] [--wait] [--json]\n andeo source merge <status|resume|cancel|take-over> --merge smi_... [options]\n\nRequest exact fast-forward landing. Native review remains the default. Administrator-authorized source-only mode requires saved policy and a current administrator; Andeo does not independently verify checks. Inspect refs and blockers with --dry-run before authorizing. Waiting is bounded and never cancels hosted work.\n\nExamples:\n andeo source merge --head feature/copy --title \"Update copy\" --idempotency-key account-copy-v1 --dry-run --json\n andeo source merge --head feature/copy --title \"Update copy\" --idempotency-key account-copy-v1 --wait --json",
|
|
2965
|
+
"source merge status": "Usage: andeo source merge status --merge smi_... [--json]\n\nRead the retained source, validation, authorization, continuation and cleanup outcomes. Source-only merged is successful completion.",
|
|
2966
|
+
"source merge resume": "Usage: andeo source merge resume --merge smi_... [--json]\n\nResume the same exact eligible intent after revalidating authority. Never substitutes a new source snapshot.",
|
|
2967
|
+
"source merge cancel": "Usage: andeo source merge cancel --merge smi_... [--json]\n\nWithdraw new work. Already-issued effects require readback; cancellation cannot undo a merge.",
|
|
2968
|
+
"source merge take-over": "Usage: andeo source merge take-over --merge smi_... --idempotency-key KEY [--json]\n\nAdministrator only. Take ownership of the retained exact source snapshot before resuming.",
|
|
2969
|
+
"source merge-policy": "Usage: andeo source merge-policy <show|set> [options]",
|
|
2970
|
+
"source change resume": "Usage: andeo source change resume --change scr_... [--json]\n\nResume the same eligible merge intent or exact failed publication. Revalidates authority and retained lineage; never creates a replacement build.\n\nExample:\n andeo source change resume --change scr_... --json",
|
|
2971
|
+
"source merge-policy show": "Usage: andeo source merge-policy show [--json]\n\nRead the saved merge policy and explicit principal grants.",
|
|
2972
|
+
"source merge-policy set": "Usage: andeo source merge-policy set --mode human-review|agent|administrator-authorized [--principals PRINCIPAL_IDS] --expected-revision N --idempotency-key KEY [--json]\n\nAdministrator only. Agent grants are comma-separated stable principal IDs. Administrator-authorized mode requires controlled transport, source-only continuation and exact per-operation approval; omit --principals. This never changes publication policy.\n\nExample:\n andeo source merge-policy set --mode agent --principals principal_example --expected-revision 0 --idempotency-key merge-policy-v1 --json",
|
|
2714
2973
|
"source change create": "Usage: andeo source change create --head BRANCH --title TITLE [--description TEXT] [--base BRANCH] [--json]\n\nOpen a review for the exact protected-base and feature-branch SHAs. Andeo binds checks and approval to this immutable snapshot.",
|
|
2715
2974
|
"source change list": "Usage: andeo source change list [--state open|closed|all] [--json]\n\nList reviewed source changes for this exact linked project.",
|
|
2716
2975
|
"source change show": "Usage: andeo source change show --change scr_... [--json]\n\nRead exact snapshot, preview, decision, and landing state.",
|