@letterstory/cli 0.121.0 → 0.121.2
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 +5 -0
- package/lib/cli.mjs +29 -4
- package/lib/commands/arena.mjs +43 -2
- package/lib/commands/collection.mjs +77 -1
- package/lib/commands/kernel.mjs +36 -5
- package/lib/commands/org.mjs +22 -1
- package/lib/commands/phantom.mjs +9 -0
- package/lib/commands/repo.mjs +41 -1
- package/lib/commands/series.mjs +98 -17
- package/lib/commands/shredder.mjs +40 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -568,6 +568,8 @@ letterstory arena endpoint delete <arena-id> --yes
|
|
|
568
568
|
letterstory arena file <arena-id> > arena.json # the file the arena runs now
|
|
569
569
|
letterstory arena file <arena-id> --patch '{"rules":{"context-figures":"measure"}}' --note "try it"
|
|
570
570
|
letterstory arena file <arena-id> --file arena.json # save a whole file as the next version
|
|
571
|
+
letterstory arena file <arena-id> --file arena.json --candidate # save beside live; runs nothing yet
|
|
572
|
+
letterstory arena promote <arena-id> <version> --yes # ship a candidate, or roll back
|
|
571
573
|
letterstory arena versions <arena-id> # your copy's versions and our releases
|
|
572
574
|
letterstory arena pin <arena-id> <version-id|none> # pin one of our releases, or follow live
|
|
573
575
|
letterstory arena offer <arena-id> [--accept] # our newer version, over your own changes
|
|
@@ -621,6 +623,8 @@ letterstory shredder job <job-id> # poll it your
|
|
|
621
623
|
# The shredder as ONE versioned file (like a kernel file; schema at /api/schema/shredder):
|
|
622
624
|
letterstory shredder file <endpoint-id> > shredder.json # the file the endpoint shreds with
|
|
623
625
|
letterstory shredder file <endpoint-id> --patch '{"rewriters":{"excludeWriterFamily":true}}'
|
|
626
|
+
letterstory shredder file <endpoint-id> --file s.json --candidate # save beside live; shreds nothing yet
|
|
627
|
+
letterstory shredder promote <endpoint-id> <version> --yes # ship a candidate, or roll back
|
|
624
628
|
letterstory shredder versions <endpoint-id> # your copy's versions and our releases
|
|
625
629
|
letterstory shredder pin <endpoint-id> <version-id|none> # pin one of our releases, or follow live
|
|
626
630
|
letterstory shredder offer <endpoint-id> [--accept] # our newer version, over your own changes
|
|
@@ -659,6 +663,7 @@ letterstory org export > acme.json # the bundle — the same doc
|
|
|
659
663
|
letterstory org export --sections primitives.kernels --sections data.collections
|
|
660
664
|
letterstory org export --split acme/ # one file per record, for keeping config in git
|
|
661
665
|
letterstory org join acme/ > acme.json # rebuild the identical bundle from the directory
|
|
666
|
+
letterstory org fields data.collections # what each setting means: label, path, how it's edited, its gate
|
|
662
667
|
letterstory org diff acme.json # what applying the bundle would change: + add, ~ change, - only in the org
|
|
663
668
|
letterstory org apply acme.json --dry-run # what apply would write; then drop --dry-run to apply
|
|
664
669
|
letterstory org apply acme.json --prune --yes # also delete records the bundle lacks
|
package/lib/cli.mjs
CHANGED
|
@@ -60,13 +60,15 @@ import {
|
|
|
60
60
|
} from "./commands.mjs";
|
|
61
61
|
|
|
62
62
|
// Keep in sync with cli/package.json.
|
|
63
|
-
export const VERSION = "0.121.
|
|
63
|
+
export const VERSION = "0.121.2";
|
|
64
64
|
|
|
65
65
|
// Flags that never take a value. Listing them explicitly means `deploy get --json <id>`
|
|
66
66
|
// can't accidentally swallow the id as --json's value. Every `type: "bool"` flag (and its
|
|
67
67
|
// `negate` spelling) in a command's ARGS map must be listed; boolean-flags.test.ts enforces it.
|
|
68
68
|
export const BOOLEAN_FLAGS = new Set([
|
|
69
69
|
"json",
|
|
70
|
+
// shredder file / arena file: save the version as a candidate beside live.
|
|
71
|
+
"candidate",
|
|
70
72
|
// site tree: include children a fold-by-default family would hide; items over the whole subtree.
|
|
71
73
|
"unfold",
|
|
72
74
|
"subtree",
|
|
@@ -105,6 +107,11 @@ export const BOOLEAN_FLAGS = new Set([
|
|
|
105
107
|
"prune",
|
|
106
108
|
// org git sync --force
|
|
107
109
|
"force",
|
|
110
|
+
// `series create --paused`; `series update <id> --pause | --resume | --retire`.
|
|
111
|
+
"paused",
|
|
112
|
+
"pause",
|
|
113
|
+
"resume",
|
|
114
|
+
"retire",
|
|
108
115
|
"buy",
|
|
109
116
|
"rebuild",
|
|
110
117
|
// phantom spread-looks --apply
|
|
@@ -178,6 +185,8 @@ export const BOOLEAN_FLAGS = new Set([
|
|
|
178
185
|
"retired",
|
|
179
186
|
"retire",
|
|
180
187
|
"restore",
|
|
188
|
+
// `collection add "<headline>" --collection <id> --queue`.
|
|
189
|
+
"queue",
|
|
181
190
|
// Bool flags from command ARGS maps that were missing here.
|
|
182
191
|
"apply-rules",
|
|
183
192
|
"async",
|
|
@@ -332,6 +341,14 @@ Collections (the content engine as a primitive — one door, 'collection run'):
|
|
|
332
341
|
publish → (hosted) rebuild. Spends one of the
|
|
333
342
|
week's cadence slots. 20–60 min; followed unless
|
|
334
343
|
--no-wait; stops at the gate under manual approval.
|
|
344
|
+
collection queue <suggestion-id> Queue a headline, as the app's queue button does: the
|
|
345
|
+
next slot of the schedule when the Planner is on,
|
|
346
|
+
else a draft to write by hand
|
|
347
|
+
collection add "<headline>" --collection <id> [--queue]
|
|
348
|
+
A headline you wrote: joins Up next, or with --queue
|
|
349
|
+
is queued at once (the next slot, or a draft)
|
|
350
|
+
collection headline <suggestion-id> [--dismiss | --later | --restore] [--headline "…"]
|
|
351
|
+
Dismiss, park, bring back or reword a headline
|
|
335
352
|
collection run-status <run-id> Status, steps, constraints, verification
|
|
336
353
|
collection runs [--collection <id>] [--status <s>] [--limit <n>]
|
|
337
354
|
collection approve <run-id> [--now] Answer the gate (--now publishes immediately)
|
|
@@ -349,6 +366,7 @@ Phantomstory (a collection Letterstory hosts as an independent publication):
|
|
|
349
366
|
'collection run --collection <its collection_id>'.
|
|
350
367
|
phantom get <phantom-id> Status, URL, domain, DNS
|
|
351
368
|
phantom config <phantom-id> [--name … --theme … --collection …] [--collection-config '<json>']
|
|
369
|
+
[--style '<json>'|none] [--stock-cover '<json>'|none] [--logo '<pick json>']
|
|
352
370
|
[--configure [--dry-run]]
|
|
353
371
|
The site's file joined to its collection's
|
|
354
372
|
phantom rebuild <phantom-id> · phantom diagnostics <phantom-id> · phantom balance
|
|
@@ -492,7 +510,11 @@ Offerings (context layered org → offering → collection; a layer sets only wh
|
|
|
492
510
|
onboarding status Show the onboarding checklist
|
|
493
511
|
onboarding step [--current <step>] [--complete <step>] [--skip <step>] [--status <status>]
|
|
494
512
|
series list The org's content series + coverage state
|
|
495
|
-
series create --name <n> --description <d> [--coverage a,b,c] [--collection <uuid>] [--template <key>]
|
|
513
|
+
series create --name <n> [--description <d>] [--coverage a,b,c] [--collection <uuid>] [--template <key>]
|
|
514
|
+
[--templates a,b] [--every-days <n>] [--purpose <text>] [--paused]
|
|
515
|
+
series update <series-id> [--every-days <n>|default] [--templates a,b] [--template <key>] [--purpose <text>]
|
|
516
|
+
[--description <d>] [--name <n>] [--coverage a,b,c] [--pause|--resume] [--retire]
|
|
517
|
+
Pace, templates, pause; --retire is the delete
|
|
496
518
|
series backfill <series-id> [--collection <uuid>] Reconstruct a series from existing posts
|
|
497
519
|
|
|
498
520
|
Ranking targets (what the org wants to be THE answer for — company-wide or on one offering):
|
|
@@ -992,9 +1014,11 @@ Shredder (diversify content produced outside Letterstory; structure preserved):
|
|
|
992
1014
|
shredder runs [--endpoint <id>] [--limit <n>] Recent shred calls (telemetry), newest first:
|
|
993
1015
|
the shredder version, level, seats and models each ran
|
|
994
1016
|
shredder file <endpoint-id> Print the shredder file the endpoint shreds with
|
|
995
|
-
shredder file <endpoint-id> (--file f.json | --patch '<json>') [--note "…"]
|
|
1017
|
+
shredder file <endpoint-id> (--file f.json | --patch '<json>') [--note "…"] [--candidate]
|
|
996
1018
|
Save a new version of the endpoint's own copy
|
|
997
|
-
(schema: GET /api/schema/shredder)
|
|
1019
|
+
(schema: GET /api/schema/shredder); --candidate
|
|
1020
|
+
stores it beside live without changing a shred
|
|
1021
|
+
shredder promote <endpoint-id> <version> --yes Make a version live: ship a candidate, or roll back
|
|
998
1022
|
shredder versions <endpoint-id> Your copy's versions and our released ones
|
|
999
1023
|
shredder pin <endpoint-id> <version-id|none> Pin one of our versions, or follow our live one
|
|
1000
1024
|
shredder offer <endpoint-id> [--accept] What our newer version would change; --accept takes it
|
|
@@ -1238,6 +1262,7 @@ Repo changes (Claude Code edits your repo in a sandbox; the Letterstory GitHub A
|
|
|
1238
1262
|
Say which site a repo builds (--not: correct a wrong label)
|
|
1239
1263
|
repo reset-site --repo <owner/name> --site <host> Back to automatic detection
|
|
1240
1264
|
repo status <run-id> Phase, spend, PR link and Claude's summary
|
|
1265
|
+
repo activity <run-id> [--follow] What the sandbox is doing, line by line (--follow: until it stops)
|
|
1241
1266
|
repo list [--status running|succeeded|no_changes|failed|cancelled] [--limit n] [--audit <id>]
|
|
1242
1267
|
[--source site_audit_issue|site_quick_check|letterprove_install]
|
|
1243
1268
|
repo publish <run-id> Make PR on a preview in review
|
package/lib/commands/arena.mjs
CHANGED
|
@@ -197,11 +197,17 @@ export const ARGS = {
|
|
|
197
197
|
get_arena_file: {
|
|
198
198
|
arena_id: { positional: 0, label: "arena-id", required: true },
|
|
199
199
|
},
|
|
200
|
+
// Live unless --candidate: a candidate changes nothing until `arena promote` ships it.
|
|
200
201
|
save_arena_file: {
|
|
201
202
|
arena_id: { positional: 0, label: "arena-id", required: true },
|
|
202
203
|
file: { value: (flags) => readJsonFileFlag(flags, "file") },
|
|
203
204
|
patch: { value: (flags) => readJsonObjectFlag(flags, "patch") },
|
|
204
205
|
note: "note",
|
|
206
|
+
status: { value: (flags) => (flags.candidate === true ? "candidate" : undefined) },
|
|
207
|
+
},
|
|
208
|
+
promote_arena_version: {
|
|
209
|
+
arena_id: { positional: 0, label: "arena-id", required: true },
|
|
210
|
+
version: { value: (_flags, positionals) => positiveInt(positionals[1], "version") },
|
|
205
211
|
},
|
|
206
212
|
list_arena_versions: {
|
|
207
213
|
arena_id: { positional: 0, label: "arena-id", required: true },
|
|
@@ -255,13 +261,15 @@ export async function cmdArena(ctx) {
|
|
|
255
261
|
return arenaFile(rest);
|
|
256
262
|
case "versions":
|
|
257
263
|
return arenaTool(rest, "list_arena_versions");
|
|
264
|
+
case "promote":
|
|
265
|
+
return arenaPromote(rest);
|
|
258
266
|
case "pin":
|
|
259
267
|
return arenaTool(rest, "pin_arena_version");
|
|
260
268
|
case "offer":
|
|
261
269
|
return rest.flags.accept === true ? arenaTool(rest, "accept_arena_offer") : arenaOffer(rest);
|
|
262
270
|
default:
|
|
263
271
|
throw new CliError(
|
|
264
|
-
`Unknown arena subcommand: ${sub ?? "(none)"}. Try: run, run-status, runs, latest, direction, cancel, balance, list, endpoint, file, versions, pin, offer, roles, role`
|
|
272
|
+
`Unknown arena subcommand: ${sub ?? "(none)"}. Try: run, run-status, runs, latest, direction, cancel, balance, list, endpoint, file, promote, versions, pin, offer, roles, role`
|
|
265
273
|
);
|
|
266
274
|
}
|
|
267
275
|
}
|
|
@@ -450,7 +458,13 @@ async function arenaFile(ctx) {
|
|
|
450
458
|
"save_arena_file",
|
|
451
459
|
argsFromFlags(ARGS.save_arena_file, flags, ctx.positionals)
|
|
452
460
|
);
|
|
453
|
-
|
|
461
|
+
const saved = toolField(result, "saved");
|
|
462
|
+
ok(
|
|
463
|
+
ctx,
|
|
464
|
+
saved?.status === "candidate"
|
|
465
|
+
? `Saved ${saved.label} as a candidate; ship it: ${ctx.bin} arena promote ${ctx.positionals[0]} ${saved.version} --yes`
|
|
466
|
+
: `Saved ${saved?.label ?? toolField(result, "version")?.label ?? "a new version"}; the arena runs it from now on.`
|
|
467
|
+
);
|
|
454
468
|
printResult(io, flags, result);
|
|
455
469
|
return 0;
|
|
456
470
|
}
|
|
@@ -520,3 +534,30 @@ async function arenaEndpoint(ctx) {
|
|
|
520
534
|
}
|
|
521
535
|
throw new CliError(`Unknown arena endpoint action: ${action ?? "(none)"}. Try: create, update, delete, config`);
|
|
522
536
|
}
|
|
537
|
+
|
|
538
|
+
function positiveInt(raw, label) {
|
|
539
|
+
if (raw === undefined) throw new CliError(`Missing required <${label}>`);
|
|
540
|
+
const n = Number(raw);
|
|
541
|
+
if (!Number.isInteger(n) || n < 1) throw new CliError(`<${label}> must be a positive whole number (got "${raw}")`);
|
|
542
|
+
return n;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/** `arena promote <arena-id> <version> --yes` — make a version live: ship a candidate, or roll back. */
|
|
546
|
+
async function arenaPromote(ctx) {
|
|
547
|
+
const { client, positionals, flags, io } = ctx;
|
|
548
|
+
const args = argsFromFlags(ARGS.promote_arena_version, flags, positionals);
|
|
549
|
+
if (!flags.yes) {
|
|
550
|
+
io.error(
|
|
551
|
+
`Every run of this arena will use version ${args.version} from its next call. Re-run with --yes to confirm: ${ctx.bin} arena promote ${args.arena_id} ${args.version} --yes`
|
|
552
|
+
);
|
|
553
|
+
return 1;
|
|
554
|
+
}
|
|
555
|
+
const result = await client.callTool("promote_arena_version", args);
|
|
556
|
+
const previous = toolField(result, "previous_live");
|
|
557
|
+
ok(
|
|
558
|
+
ctx,
|
|
559
|
+
`Version ${args.version} is live.${previous ? ` To undo: ${ctx.bin} arena promote ${args.arena_id} ${previous} --yes` : ""}`
|
|
560
|
+
);
|
|
561
|
+
printResult(io, flags, result);
|
|
562
|
+
return 0;
|
|
563
|
+
}
|
|
@@ -87,6 +87,22 @@ export const ARGS = {
|
|
|
87
87
|
rebuild: { flag: "no-rebuild", type: "bool", invert: true },
|
|
88
88
|
schedule_for: { flag: "at" },
|
|
89
89
|
},
|
|
90
|
+
queue_suggestion: {
|
|
91
|
+
suggestion_id: { positional: 0, label: "suggestion-id", required: true },
|
|
92
|
+
},
|
|
93
|
+
add_headline: {
|
|
94
|
+
collection_id: { flag: "collection", required: true },
|
|
95
|
+
headline: { positional: 0, label: "headline", required: true },
|
|
96
|
+
queue: { flag: "queue", type: "bool" },
|
|
97
|
+
},
|
|
98
|
+
update_suggestion: {
|
|
99
|
+
suggestion_id: { positional: 0, label: "suggestion-id", required: true },
|
|
100
|
+
status: {
|
|
101
|
+
value: (flags) =>
|
|
102
|
+
flags.dismiss ? "dismissed" : flags.later ? "backlog" : flags.restore ? "suggested" : undefined,
|
|
103
|
+
},
|
|
104
|
+
headline: { flag: "headline" },
|
|
105
|
+
},
|
|
90
106
|
get_collection_run: {
|
|
91
107
|
run_id: { positional: 0, label: "run-id", required: true },
|
|
92
108
|
},
|
|
@@ -143,6 +159,12 @@ export async function cmdCollection(ctx) {
|
|
|
143
159
|
return collectionDelete(rest);
|
|
144
160
|
case "run":
|
|
145
161
|
return collectionRun(rest);
|
|
162
|
+
case "queue":
|
|
163
|
+
return collectionQueue(rest);
|
|
164
|
+
case "headline":
|
|
165
|
+
return collectionHeadline(rest);
|
|
166
|
+
case "add":
|
|
167
|
+
return collectionAdd(rest);
|
|
146
168
|
case "run-status":
|
|
147
169
|
case "status":
|
|
148
170
|
return collectionRunStatus(rest);
|
|
@@ -160,7 +182,7 @@ export async function cmdCollection(ctx) {
|
|
|
160
182
|
return collectionLinks(rest);
|
|
161
183
|
default:
|
|
162
184
|
throw new CliError(
|
|
163
|
-
`Unknown collection subcommand: ${sub ?? "(none)"}. Try: list, overview, new, config, delete, run, run-status, runs, approve, cancel, inventory, links`
|
|
185
|
+
`Unknown collection subcommand: ${sub ?? "(none)"}. Try: list, overview, new, config, delete, run, add, queue, headline, run-status, runs, approve, cancel, inventory, links`
|
|
164
186
|
);
|
|
165
187
|
}
|
|
166
188
|
}
|
|
@@ -266,6 +288,60 @@ async function collectionRun(ctx) {
|
|
|
266
288
|
return 0;
|
|
267
289
|
}
|
|
268
290
|
|
|
291
|
+
// `collection headline <suggestion-id> [--dismiss | --later | --restore] [--headline "…"]` —
|
|
292
|
+
// the app's Dismiss and Edit on a suggested headline (update_suggestion).
|
|
293
|
+
async function collectionHeadline(ctx) {
|
|
294
|
+
const { client, flags, io } = ctx;
|
|
295
|
+
refuseUnknownFlags(ctx, ARGS.update_suggestion, ["dismiss", "later", "restore"]);
|
|
296
|
+
const result = await client.callTool(
|
|
297
|
+
"update_suggestion",
|
|
298
|
+
argsFromFlags(ARGS.update_suggestion, flags, ctx.positionals)
|
|
299
|
+
);
|
|
300
|
+
ok(ctx, "Headline updated.");
|
|
301
|
+
printResult(io, flags, result);
|
|
302
|
+
return 0;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// `collection add "<headline>" --collection <id> [--queue]` — the letterbase page's New post: a
|
|
306
|
+
// headline you wrote joins Up next, or with --queue takes the next slot (a draft, planner off).
|
|
307
|
+
async function collectionAdd(ctx) {
|
|
308
|
+
const { client, flags, io } = ctx;
|
|
309
|
+
const result = await client.callTool("add_headline", argsFromFlags(ARGS.add_headline, flags, ctx.positionals));
|
|
310
|
+
const queued = toolField(result, "queued");
|
|
311
|
+
ok(
|
|
312
|
+
ctx,
|
|
313
|
+
!queued
|
|
314
|
+
? "Added to Up next."
|
|
315
|
+
: queued.via === "slot"
|
|
316
|
+
? `Queued for ${queued.scheduled_for ?? "?"} (run ${queued.run_id ?? "?"}).`
|
|
317
|
+
: `The Planner is off: drafted as post ${queued.article?.id ?? "?"}.`
|
|
318
|
+
);
|
|
319
|
+
printResult(io, flags, result);
|
|
320
|
+
return 0;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// `collection queue <suggestion-id>` — the app's queue button: the Planner's next slot when
|
|
324
|
+
// it is on (a run that writes on the slot's lead time), else a draft to write by hand.
|
|
325
|
+
async function collectionQueue(ctx) {
|
|
326
|
+
const { client, flags, io, bin } = ctx;
|
|
327
|
+
const result = await client.callTool(
|
|
328
|
+
"queue_suggestion",
|
|
329
|
+
argsFromFlags(ARGS.queue_suggestion, flags, ctx.positionals)
|
|
330
|
+
);
|
|
331
|
+
const via = toolField(result, "via");
|
|
332
|
+
if (via === "slot") {
|
|
333
|
+
const runId = toolField(result, "run_id");
|
|
334
|
+
ok(
|
|
335
|
+
ctx,
|
|
336
|
+
`Queued on the schedule for ${toolField(result, "scheduled_for") ?? "?"} (run ${runId ?? "?"}). Follow it with: ${bin} collection run-status ${runId ?? "<run-id>"}`
|
|
337
|
+
);
|
|
338
|
+
} else {
|
|
339
|
+
ok(ctx, `The Planner is off: drafted as post ${toolField(result, "article")?.id ?? "?"}.`);
|
|
340
|
+
}
|
|
341
|
+
printResult(io, flags, result);
|
|
342
|
+
return 0;
|
|
343
|
+
}
|
|
344
|
+
|
|
269
345
|
async function collectionRunStatus(ctx) {
|
|
270
346
|
const result = await ctx.client.callTool(
|
|
271
347
|
"get_collection_run",
|
package/lib/commands/kernel.mjs
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
// unless you tell it not to.
|
|
19
19
|
|
|
20
20
|
import { readFileSync } from "node:fs";
|
|
21
|
+
import { basename } from "node:path";
|
|
21
22
|
import { CliError } from "../client.mjs";
|
|
22
23
|
import {
|
|
23
24
|
flagStr,
|
|
@@ -241,8 +242,25 @@ export const ARGS = {
|
|
|
241
242
|
source_kernel_id: { positional: 0, label: "source-kernel-id", required: true },
|
|
242
243
|
name: "name",
|
|
243
244
|
},
|
|
245
|
+
// `--name` names the new kernel (over the file's own name); `--from` starts from a kernel you can
|
|
246
|
+
// see; `--skill SKILL.md` makes one from your own writing instructions.
|
|
244
247
|
create_kernel: {
|
|
245
|
-
file: {
|
|
248
|
+
file: {
|
|
249
|
+
value: (flags) => {
|
|
250
|
+
const file = readJsonFileFlag(flags, "file");
|
|
251
|
+
const name = flagStr(flags.name);
|
|
252
|
+
if (name === undefined) return file;
|
|
253
|
+
return { ...(file ?? {}), name };
|
|
254
|
+
},
|
|
255
|
+
},
|
|
256
|
+
from: "from",
|
|
257
|
+
skill: { value: (flags) => readTextFileFlag(flags, "skill") },
|
|
258
|
+
skill_filename: {
|
|
259
|
+
value: (flags) => {
|
|
260
|
+
const path = flagStr(flags.skill);
|
|
261
|
+
return path === undefined ? undefined : basename(path);
|
|
262
|
+
},
|
|
263
|
+
},
|
|
246
264
|
},
|
|
247
265
|
get_org_writing_kernel: {},
|
|
248
266
|
set_org_writing_kernel: {
|
|
@@ -767,15 +785,28 @@ async function kernelFork(ctx) {
|
|
|
767
785
|
return 0;
|
|
768
786
|
}
|
|
769
787
|
|
|
770
|
-
/**
|
|
788
|
+
/**
|
|
789
|
+
* `kernel create` — a kernel your org owns, from one of three starts (combinable):
|
|
790
|
+
* --file kernel.json a kernel file (a name and a voice is enough)
|
|
791
|
+
* --from <kernel-id|name> a kernel you can see: your current one for a new kernel, any other to fork
|
|
792
|
+
* --skill SKILL.md your own writing instructions, over --from or else your current kernel
|
|
793
|
+
* `--name` names it.
|
|
794
|
+
*/
|
|
771
795
|
async function kernelCreate(ctx) {
|
|
772
796
|
const { client, positionals, flags, io } = ctx;
|
|
773
|
-
if (flagStr(flags.file) === undefined) {
|
|
774
|
-
throw new CliError(
|
|
797
|
+
if (flagStr(flags.file) === undefined && flagStr(flags.from) === undefined && flagStr(flags.skill) === undefined) {
|
|
798
|
+
throw new CliError(
|
|
799
|
+
'Provide --file <path> (as short as {"name": "…", "voice": "…"}), --from <kernel-id|name> or --skill <SKILL.md>.'
|
|
800
|
+
);
|
|
775
801
|
}
|
|
776
802
|
const result = await client.callTool("create_kernel", argsFromFlags(ARGS.create_kernel, flags, positionals));
|
|
777
803
|
const kernel = toolField(result, "kernel");
|
|
778
|
-
|
|
804
|
+
const basedOn = toolField(result, "based_on");
|
|
805
|
+
if (kernel?.id)
|
|
806
|
+
ok(
|
|
807
|
+
ctx,
|
|
808
|
+
`Created "${kernel.name}" (${kernel.id}), version 1, live${basedOn ? `, from ${basedOn.name} v${basedOn.version}` : ""}.`
|
|
809
|
+
);
|
|
779
810
|
printResult(io, flags, result);
|
|
780
811
|
return 0;
|
|
781
812
|
}
|
package/lib/commands/org.mjs
CHANGED
|
@@ -25,6 +25,7 @@ export const ARGS = {
|
|
|
25
25
|
allow: { flag: "allow", type: "list" },
|
|
26
26
|
confirm: { flag: "confirm" },
|
|
27
27
|
},
|
|
28
|
+
describe_org_config_fields: {},
|
|
28
29
|
get_org_config_source: {},
|
|
29
30
|
list_integrations: {},
|
|
30
31
|
get_linear_connection: {},
|
|
@@ -63,11 +64,13 @@ export async function cmdOrg(ctx) {
|
|
|
63
64
|
return orgGit(rest);
|
|
64
65
|
case "integrations":
|
|
65
66
|
return orgIntegrations(rest);
|
|
67
|
+
case "fields":
|
|
68
|
+
return orgFields(rest);
|
|
66
69
|
case "linear":
|
|
67
70
|
return orgLinear(rest);
|
|
68
71
|
default:
|
|
69
72
|
throw new CliError(
|
|
70
|
-
`Unknown org subcommand: ${sub ?? "(none)"}. Try: export, join, diff, apply, git, integrations, linear`
|
|
73
|
+
`Unknown org subcommand: ${sub ?? "(none)"}. Try: export, join, diff, apply, git, integrations, linear, fields`
|
|
71
74
|
);
|
|
72
75
|
}
|
|
73
76
|
}
|
|
@@ -84,6 +87,24 @@ async function orgIntegrations(ctx) {
|
|
|
84
87
|
return 0;
|
|
85
88
|
}
|
|
86
89
|
|
|
90
|
+
/** `org fields [section]`: what each setting in the bundle means (describe_org_config_fields). */
|
|
91
|
+
async function orgFields(ctx) {
|
|
92
|
+
const { client, flags, io, positionals } = ctx;
|
|
93
|
+
refuseUnknownFlags(ctx, {}, []);
|
|
94
|
+
const section = positionals[0];
|
|
95
|
+
const result = await client.callTool("describe_org_config_fields", {});
|
|
96
|
+
printResult(io, flags, result, (r) =>
|
|
97
|
+
r.fields
|
|
98
|
+
.filter((f) => !section || f.section === section)
|
|
99
|
+
.map(
|
|
100
|
+
(f) =>
|
|
101
|
+
`${f.section} ${f.path} ${f.label}${f.readOnly ? " (read-only)" : ""}${f.gate ? ` [${f.gate}]` : ""}`
|
|
102
|
+
)
|
|
103
|
+
.join("\n")
|
|
104
|
+
);
|
|
105
|
+
return 0;
|
|
106
|
+
}
|
|
107
|
+
|
|
87
108
|
/**
|
|
88
109
|
* `org linear` — where Letter files Linear issues (connecting is OAuth, in Settings):
|
|
89
110
|
* org linear the connection (get_linear_connection)
|
package/lib/commands/phantom.mjs
CHANGED
|
@@ -42,6 +42,10 @@ function readJsonObjectFlag(flags, name) {
|
|
|
42
42
|
}
|
|
43
43
|
return readJsonFileFlag(flags, `${name}-file`);
|
|
44
44
|
}
|
|
45
|
+
/** `--<name> none` → null (clear it); otherwise the JSON object, inline or from `--<name>-file`. */
|
|
46
|
+
function readJsonObjectOrNone(flags, name) {
|
|
47
|
+
return flagStr(flags[name]) === "none" ? null : readJsonObjectFlag(flags, name);
|
|
48
|
+
}
|
|
45
49
|
function stripScheme(domain) {
|
|
46
50
|
return String(domain)
|
|
47
51
|
.replace(/^https?:\/\//i, "")
|
|
@@ -79,6 +83,11 @@ export const ARGS = {
|
|
|
79
83
|
value: (flags) =>
|
|
80
84
|
flags.layout === undefined ? undefined : flags.layout === "none" ? null : String(flags.layout),
|
|
81
85
|
},
|
|
86
|
+
// The site's palette and fonts, stock covers ('{"enabled":true}'), each `none` to clear;
|
|
87
|
+
// a logo pick ('{"spec":{"archetype":"geometric","variantId":"…"}}') to compose.
|
|
88
|
+
style: { value: (flags) => readJsonObjectOrNone(flags, "style") },
|
|
89
|
+
stock_cover: { value: (flags) => readJsonObjectOrNone(flags, "stock-cover") },
|
|
90
|
+
logo: { value: (flags) => readJsonObjectFlag(flags, "logo") },
|
|
82
91
|
maverick_enabled: { flag: "maverick", type: "bool", negate: "no-maverick" },
|
|
83
92
|
// `--maverick-customer-id none` clears it (identification back to pending).
|
|
84
93
|
maverick_customer_id: {
|
package/lib/commands/repo.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// `repo` — changes to the org's own repos, made by Claude Code in a sandbox and opened as a
|
|
2
2
|
// pull request by the Letterstory GitHub App (src/lib/repo-changes). One subcommand per tool:
|
|
3
3
|
// repo start → start_repo_change repo status → get_repo_change
|
|
4
|
+
// repo activity → get_repo_change_activity (what the sandbox is doing; --follow polls)
|
|
4
5
|
// repo list → list_repo_changes repo cancel → cancel_repo_change
|
|
5
6
|
// repo publish → publish_repo_change (Make PR on a preview)
|
|
6
7
|
// repo repos → list_github_app_repos repo branches → list_repo_branches
|
|
@@ -138,13 +139,17 @@ const ARGS = {
|
|
|
138
139
|
autopilot_min_severity: { flag: "autopilot-severity", enum: ["critical", "warning"] },
|
|
139
140
|
},
|
|
140
141
|
get_repo_change: { run_id: { positional: 0, required: true, label: "run-id" } },
|
|
142
|
+
get_repo_change_activity: { run_id: { positional: 0, required: true, label: "run-id" } },
|
|
141
143
|
cancel_repo_change: { run_id: { positional: 0, required: true, label: "run-id" } },
|
|
142
144
|
publish_repo_change: { run_id: { positional: 0, required: true, label: "run-id" } },
|
|
143
145
|
list_repo_changes: {
|
|
144
146
|
status: { flag: "status", enum: ["running", "succeeded", "no_changes", "failed", "cancelled", "sent"] },
|
|
145
147
|
limit: { flag: "limit", type: "num" },
|
|
146
148
|
audit_id: "audit",
|
|
147
|
-
source_kind: {
|
|
149
|
+
source_kind: {
|
|
150
|
+
flag: "source",
|
|
151
|
+
enum: ["site_audit_issue", "site_quick_check", "letterprove_install", "collection_publish"],
|
|
152
|
+
},
|
|
148
153
|
site_host: "site",
|
|
149
154
|
},
|
|
150
155
|
list_github_app_repos: {},
|
|
@@ -181,6 +186,8 @@ export async function cmdRepo(ctx) {
|
|
|
181
186
|
return repoStart(rest);
|
|
182
187
|
case "status":
|
|
183
188
|
return repoStatus(rest);
|
|
189
|
+
case "activity":
|
|
190
|
+
return repoActivity(rest);
|
|
184
191
|
case "fix-audit":
|
|
185
192
|
return repoFixAudit(rest);
|
|
186
193
|
case "install-letterprove":
|
|
@@ -404,6 +411,39 @@ async function repoStatus(ctx) {
|
|
|
404
411
|
return 0;
|
|
405
412
|
}
|
|
406
413
|
|
|
414
|
+
const ACTIVITY_MARK = { progress: "·", stage: "▸", say: "»", tool: "$", error: "!" };
|
|
415
|
+
|
|
416
|
+
/** `repo activity <run-id> [--follow]`: the build's feed; --follow prints new lines until it stops. */
|
|
417
|
+
async function repoActivity(ctx) {
|
|
418
|
+
const { client, flags, io } = ctx;
|
|
419
|
+
refuseUnknownFlags(ctx, {}, ["follow"]);
|
|
420
|
+
const args = argsFromFlags(ARGS.get_repo_change_activity, flags, ctx.positionals);
|
|
421
|
+
const line = (l) => `${l.at.slice(11, 19)} ${ACTIVITY_MARK[l.kind] ?? " "} ${l.text}`;
|
|
422
|
+
let { activity } = await client.callTool("get_repo_change_activity", args);
|
|
423
|
+
if (flags.json && !flags.follow) {
|
|
424
|
+
printResult(io, flags, activity);
|
|
425
|
+
return 0;
|
|
426
|
+
}
|
|
427
|
+
let seen = 0;
|
|
428
|
+
const print = () => {
|
|
429
|
+
for (const l of activity.lines.slice(seen)) io.log(flags.json ? JSON.stringify(l) : line(l));
|
|
430
|
+
seen = activity.lines.length;
|
|
431
|
+
};
|
|
432
|
+
print();
|
|
433
|
+
if (!flags.follow) return 0;
|
|
434
|
+
await followJob(
|
|
435
|
+
io,
|
|
436
|
+
async () => {
|
|
437
|
+
({ activity } = await client.callTool("get_repo_change_activity", args));
|
|
438
|
+
print();
|
|
439
|
+
return { done: !activity.live, value: activity };
|
|
440
|
+
},
|
|
441
|
+
{ intervalMs: 3000, ceilingMs: 60 * 60 * 1000 }
|
|
442
|
+
);
|
|
443
|
+
ok(ctx, `Run ${activity.runId} is ${activity.status} (${activity.phase}).`);
|
|
444
|
+
return 0;
|
|
445
|
+
}
|
|
446
|
+
|
|
407
447
|
async function repoList(ctx) {
|
|
408
448
|
const { client, flags, io } = ctx;
|
|
409
449
|
const result = await client.callTool(
|
package/lib/commands/series.mjs
CHANGED
|
@@ -2,10 +2,69 @@
|
|
|
2
2
|
// point beside topic clusters, hierarchy revamp D/G). A series is a coverage
|
|
3
3
|
// obligation: "content that looks like this", one post per declared item; the
|
|
4
4
|
// planner queue writes the gaps and backfill reconstructs what existing posts
|
|
5
|
-
// already fulfill.
|
|
5
|
+
// already fulfill. It is an automation on the collection it writes into
|
|
6
|
+
// (docs/architecture/content-databases.md): "a comparison post every 7 days" is
|
|
7
|
+
// a series with a comparison template and --every-days 7.
|
|
6
8
|
|
|
7
9
|
import { CliError } from "../client.mjs";
|
|
8
|
-
import { compact, flagStr, ok, printResult,
|
|
10
|
+
import { argsFromFlags, compact, flagStr, ok, printResult, refuseUnknownFlags, requirePositional } from "./shared.mjs";
|
|
11
|
+
|
|
12
|
+
/** `--coverage a,b,c` (or `--templates a,b`) → a list; absent → undefined. */
|
|
13
|
+
const commaList = (name) => (flags) => {
|
|
14
|
+
const v = flagStr(flags[name]);
|
|
15
|
+
return v === undefined
|
|
16
|
+
? undefined
|
|
17
|
+
: v
|
|
18
|
+
.split(",")
|
|
19
|
+
.map((c) => c.trim())
|
|
20
|
+
.filter(Boolean);
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/** `--every-days 7` → 7; `--every-days default` → null (the default pace, 7). */
|
|
24
|
+
const everyDays = (flags) => {
|
|
25
|
+
const v = flagStr(flags["every-days"]);
|
|
26
|
+
if (v === undefined) return undefined;
|
|
27
|
+
if (v === "default") return null;
|
|
28
|
+
const n = Number(v);
|
|
29
|
+
if (!Number.isInteger(n) || n < 1) throw new CliError(`--every-days must be a whole number of days or "default"`);
|
|
30
|
+
return n;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* How every command in this file sources its tool inputs — see `argsFromFlags` in
|
|
35
|
+
* shared.mjs and `scripts/parity-check.ts`, which compares these keys against each tool's
|
|
36
|
+
* published input schema.
|
|
37
|
+
*/
|
|
38
|
+
export const ARGS = {
|
|
39
|
+
create_series: {
|
|
40
|
+
name: { flag: "name", required: true },
|
|
41
|
+
description: { flag: "description" },
|
|
42
|
+
collection_id: { flag: "collection" },
|
|
43
|
+
template_key: { flag: "template" },
|
|
44
|
+
template_keys: { value: commaList("templates") },
|
|
45
|
+
coverage: { value: commaList("coverage") },
|
|
46
|
+
purpose: { flag: "purpose" },
|
|
47
|
+
every_days: { value: everyDays },
|
|
48
|
+
paused: { flag: "paused", type: "bool" },
|
|
49
|
+
},
|
|
50
|
+
update_series: {
|
|
51
|
+
series_id: { positional: 0, label: "series-id", required: true },
|
|
52
|
+
name: { flag: "name" },
|
|
53
|
+
description: { flag: "description" },
|
|
54
|
+
template_key: { flag: "template" },
|
|
55
|
+
template_keys: { value: commaList("templates") },
|
|
56
|
+
coverage: { value: commaList("coverage") },
|
|
57
|
+
purpose: { flag: "purpose" },
|
|
58
|
+
every_days: { value: everyDays },
|
|
59
|
+
paused: { flag: "pause", type: "bool", negate: "resume" },
|
|
60
|
+
// `--retire` is the delete (members keep their history). `--status` is a global value-less flag.
|
|
61
|
+
status: { value: (flags) => (flags.retire === true ? "retired" : undefined) },
|
|
62
|
+
},
|
|
63
|
+
queue_series_next: {
|
|
64
|
+
series_id: { positional: 0, label: "series-id", required: true },
|
|
65
|
+
collection_id: { flag: "collection", required: true },
|
|
66
|
+
},
|
|
67
|
+
};
|
|
9
68
|
|
|
10
69
|
export async function cmdSeries(ctx) {
|
|
11
70
|
const sub = ctx.positionals[0];
|
|
@@ -15,10 +74,16 @@ export async function cmdSeries(ctx) {
|
|
|
15
74
|
return seriesList(rest);
|
|
16
75
|
case "create":
|
|
17
76
|
return seriesCreate(rest);
|
|
77
|
+
case "update":
|
|
78
|
+
return seriesUpdate(rest);
|
|
18
79
|
case "backfill":
|
|
19
80
|
return seriesBackfill(rest);
|
|
81
|
+
case "queue":
|
|
82
|
+
return seriesQueue(rest);
|
|
20
83
|
default:
|
|
21
|
-
throw new CliError(
|
|
84
|
+
throw new CliError(
|
|
85
|
+
`Unknown series subcommand: ${sub ?? "(none)"}. Try: list, create, update, queue, backfill`
|
|
86
|
+
);
|
|
22
87
|
}
|
|
23
88
|
}
|
|
24
89
|
|
|
@@ -45,25 +110,41 @@ async function seriesList(ctx) {
|
|
|
45
110
|
|
|
46
111
|
async function seriesCreate(ctx) {
|
|
47
112
|
const { client, flags, io } = ctx;
|
|
48
|
-
|
|
49
|
-
const
|
|
50
|
-
name: requireFlag(flags, "name"),
|
|
51
|
-
description: requireFlag(flags, "description"),
|
|
52
|
-
collection_id: flagStr(flags.collection),
|
|
53
|
-
template_key: flagStr(flags.template),
|
|
54
|
-
coverage: coverage
|
|
55
|
-
? coverage
|
|
56
|
-
.split(",")
|
|
57
|
-
.map((c) => c.trim())
|
|
58
|
-
.filter(Boolean)
|
|
59
|
-
: undefined,
|
|
60
|
-
});
|
|
61
|
-
const result = await client.callTool("create_series", args);
|
|
113
|
+
refuseUnknownFlags(ctx, ARGS.create_series);
|
|
114
|
+
const result = await client.callTool("create_series", argsFromFlags(ARGS.create_series, flags, ctx.positionals));
|
|
62
115
|
ok(ctx, "Series created — the queue starts covering its items.");
|
|
63
116
|
printResult(io, flags, result);
|
|
64
117
|
return 0;
|
|
65
118
|
}
|
|
66
119
|
|
|
120
|
+
async function seriesUpdate(ctx) {
|
|
121
|
+
const { client, flags, io } = ctx;
|
|
122
|
+
refuseUnknownFlags(ctx, ARGS.update_series);
|
|
123
|
+
const args = argsFromFlags(ARGS.update_series, flags, ctx.positionals);
|
|
124
|
+
if (Object.keys(args).length < 2) {
|
|
125
|
+
throw new CliError(
|
|
126
|
+
"Nothing to update. Pass --every-days, --templates, --purpose, --description, --name, --coverage, --pause/--resume or --retire."
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
const result = await client.callTool("update_series", args);
|
|
130
|
+
ok(ctx, "Series updated.");
|
|
131
|
+
printResult(io, flags, result);
|
|
132
|
+
return 0;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** `series queue <series-id> --collection <id>`: its next installment, now (queue_series_next). */
|
|
136
|
+
async function seriesQueue(ctx) {
|
|
137
|
+
const { client, flags, io } = ctx;
|
|
138
|
+
refuseUnknownFlags(ctx, ARGS.queue_series_next);
|
|
139
|
+
const result = await client.callTool(
|
|
140
|
+
"queue_series_next",
|
|
141
|
+
argsFromFlags(ARGS.queue_series_next, flags, ctx.positionals)
|
|
142
|
+
);
|
|
143
|
+
ok(ctx, `Queued "${result?.headline ?? "the next installment"}".`);
|
|
144
|
+
printResult(io, flags, result);
|
|
145
|
+
return 0;
|
|
146
|
+
}
|
|
147
|
+
|
|
67
148
|
async function seriesBackfill(ctx) {
|
|
68
149
|
const { client, flags, io } = ctx;
|
|
69
150
|
const args = compact({
|
|
@@ -109,11 +109,17 @@ export const ARGS = {
|
|
|
109
109
|
get_shredder_file: {
|
|
110
110
|
endpoint_id: { positional: 0, label: "endpoint-id", required: true },
|
|
111
111
|
},
|
|
112
|
+
// Live unless --candidate: a candidate changes nothing until `shredder promote` ships it.
|
|
112
113
|
save_shredder_file: {
|
|
113
114
|
endpoint_id: { positional: 0, label: "endpoint-id", required: true },
|
|
114
115
|
file: { value: (flags) => readJsonFileFlag(flags, "file") },
|
|
115
116
|
patch: { value: (flags) => readJsonObjectFlag(flags, "patch") },
|
|
116
117
|
note: "note",
|
|
118
|
+
status: { value: (flags) => (flags.candidate === true ? "candidate" : undefined) },
|
|
119
|
+
},
|
|
120
|
+
promote_shredder_version: {
|
|
121
|
+
endpoint_id: { positional: 0, label: "endpoint-id", required: true },
|
|
122
|
+
version: { value: (_flags, positionals) => positiveInt(positionals[1], "version") },
|
|
117
123
|
},
|
|
118
124
|
list_shredder_versions: {
|
|
119
125
|
endpoint_id: { positional: 0, label: "endpoint-id", required: true },
|
|
@@ -163,6 +169,8 @@ export async function cmdShredder(ctx) {
|
|
|
163
169
|
return shredderFile(rest);
|
|
164
170
|
case "versions":
|
|
165
171
|
return shredderTool(rest, "list_shredder_versions");
|
|
172
|
+
case "promote":
|
|
173
|
+
return shredderPromote(rest);
|
|
166
174
|
case "pin":
|
|
167
175
|
return shredderTool(rest, "pin_shredder_version");
|
|
168
176
|
case "offer":
|
|
@@ -171,7 +179,7 @@ export async function cmdShredder(ctx) {
|
|
|
171
179
|
return shredderTool(rest, "get_shred_family_health");
|
|
172
180
|
default:
|
|
173
181
|
throw new CliError(
|
|
174
|
-
`Unknown shredder subcommand: ${sub ?? "(none)"}. Try: list, create, delete, endpoint, run, job, runs, balance, file, versions, pin, offer, families`
|
|
182
|
+
`Unknown shredder subcommand: ${sub ?? "(none)"}. Try: list, create, delete, endpoint, run, job, runs, balance, file, promote, versions, pin, offer, families`
|
|
175
183
|
);
|
|
176
184
|
}
|
|
177
185
|
}
|
|
@@ -366,9 +374,12 @@ async function shredderFile(ctx) {
|
|
|
366
374
|
"save_shredder_file",
|
|
367
375
|
argsFromFlags(ARGS.save_shredder_file, flags, ctx.positionals)
|
|
368
376
|
);
|
|
377
|
+
const saved = toolField(result, "saved");
|
|
369
378
|
ok(
|
|
370
379
|
ctx,
|
|
371
|
-
|
|
380
|
+
saved?.status === "candidate"
|
|
381
|
+
? `Saved ${saved.label} as a candidate; ship it: ${ctx.bin} shredder promote ${ctx.positionals[0]} ${saved.version} --yes`
|
|
382
|
+
: `Saved ${saved?.label ?? toolField(result, "version")?.label ?? "a new version"}; the endpoint shreds with it from now on.`
|
|
372
383
|
);
|
|
373
384
|
printResult(io, flags, result);
|
|
374
385
|
return 0;
|
|
@@ -393,3 +404,30 @@ async function shredderTool(ctx, tool) {
|
|
|
393
404
|
printResult(ctx.io, ctx.flags, result);
|
|
394
405
|
return 0;
|
|
395
406
|
}
|
|
407
|
+
|
|
408
|
+
function positiveInt(raw, label) {
|
|
409
|
+
if (raw === undefined) throw new CliError(`Missing required <${label}>`);
|
|
410
|
+
const n = Number(raw);
|
|
411
|
+
if (!Number.isInteger(n) || n < 1) throw new CliError(`<${label}> must be a positive whole number (got "${raw}")`);
|
|
412
|
+
return n;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/** `shredder promote <endpoint-id> <version> --yes` — make a version live: ship a candidate, or roll back. */
|
|
416
|
+
async function shredderPromote(ctx) {
|
|
417
|
+
const { client, positionals, flags, io } = ctx;
|
|
418
|
+
const args = argsFromFlags(ARGS.promote_shredder_version, flags, positionals);
|
|
419
|
+
if (!flags.yes) {
|
|
420
|
+
io.error(
|
|
421
|
+
`Every shred through this endpoint will use version ${args.version} from its next call. Re-run with --yes to confirm: ${ctx.bin} shredder promote ${args.endpoint_id} ${args.version} --yes`
|
|
422
|
+
);
|
|
423
|
+
return 1;
|
|
424
|
+
}
|
|
425
|
+
const result = await client.callTool("promote_shredder_version", args);
|
|
426
|
+
const previous = toolField(result, "previous_live");
|
|
427
|
+
ok(
|
|
428
|
+
ctx,
|
|
429
|
+
`Version ${args.version} is live.${previous ? ` To undo: ${ctx.bin} shredder promote ${args.endpoint_id} ${previous} --yes` : ""}`
|
|
430
|
+
);
|
|
431
|
+
printResult(io, flags, result);
|
|
432
|
+
return 0;
|
|
433
|
+
}
|