@noir-ai/cli 1.9.4 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin.js +554 -29
- package/dist/bin.js.map +1 -1
- package/dist/tui/index.js +562 -37
- package/dist/tui/index.js.map +1 -1
- package/package.json +11 -11
package/dist/bin.js
CHANGED
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
log,
|
|
28
28
|
requireInteractive,
|
|
29
29
|
spinner,
|
|
30
|
+
success,
|
|
30
31
|
table,
|
|
31
32
|
tip,
|
|
32
33
|
useColor,
|
|
@@ -44,7 +45,12 @@ import { Command, Option } from "commander";
|
|
|
44
45
|
import { Client, StreamableHTTPClientTransport } from "@modelcontextprotocol/client";
|
|
45
46
|
import { ContextEngine, createEmbedFn, resolveEmbedderConfig } from "@noir-ai/context";
|
|
46
47
|
import { loadProjectInfo, NOIR_VERSION } from "@noir-ai/core";
|
|
47
|
-
import {
|
|
48
|
+
import {
|
|
49
|
+
ensureDaemonRunning,
|
|
50
|
+
pidAlive,
|
|
51
|
+
readDaemonRecord,
|
|
52
|
+
resolveGateConfig
|
|
53
|
+
} from "@noir-ai/daemon";
|
|
48
54
|
import { createMemoryEngine } from "@noir-ai/memory";
|
|
49
55
|
import { openStore } from "@noir-ai/store";
|
|
50
56
|
import { WorkflowEngine } from "@noir-ai/workflow";
|
|
@@ -214,7 +220,12 @@ async function withInProcessRead(opts, fn) {
|
|
|
214
220
|
embed,
|
|
215
221
|
storeDegraded: true
|
|
216
222
|
});
|
|
217
|
-
const workflow = new WorkflowEngine(
|
|
223
|
+
const workflow = new WorkflowEngine(
|
|
224
|
+
store,
|
|
225
|
+
project.root,
|
|
226
|
+
project.id,
|
|
227
|
+
resolveGateConfig(project.config)
|
|
228
|
+
);
|
|
218
229
|
return await fn({ context, memory, workflow });
|
|
219
230
|
} finally {
|
|
220
231
|
await store.close().catch(() => {
|
|
@@ -605,13 +616,13 @@ async function daemonRestart(opts) {
|
|
|
605
616
|
|
|
606
617
|
// src/commands/handoff.ts
|
|
607
618
|
import { mkdirSync, writeFileSync } from "fs";
|
|
608
|
-
import { join } from "path";
|
|
619
|
+
import { join as join2 } from "path";
|
|
609
620
|
import {
|
|
610
621
|
defaultHandoffBlock,
|
|
611
622
|
hostLaunchDirective,
|
|
612
623
|
resolveAdapter
|
|
613
624
|
} from "@noir-ai/adapters";
|
|
614
|
-
import { loadProjectInfo as
|
|
625
|
+
import { loadProjectInfo as loadProjectInfo5, NOIR_DIR } from "@noir-ai/core";
|
|
615
626
|
|
|
616
627
|
// src/commands/status.ts
|
|
617
628
|
import { loadProjectInfo as loadProjectInfo3, NOIR_VERSION as NOIR_VERSION2 } from "@noir-ai/core";
|
|
@@ -728,6 +739,9 @@ function renderHuman(p, opts) {
|
|
|
728
739
|
],
|
|
729
740
|
opts
|
|
730
741
|
);
|
|
742
|
+
if (p.workflow && p.workflow.state !== "done" && p.workflow.state !== "abandoned") {
|
|
743
|
+
tip("resume: `noir task resume`", opts);
|
|
744
|
+
}
|
|
731
745
|
}
|
|
732
746
|
async function gatherStatusPayload(opts) {
|
|
733
747
|
const project = loadProjectInfo3(process.cwd());
|
|
@@ -775,7 +789,17 @@ async function status(opts) {
|
|
|
775
789
|
}
|
|
776
790
|
|
|
777
791
|
// src/commands/task.ts
|
|
778
|
-
import {
|
|
792
|
+
import { spawnSync } from "child_process";
|
|
793
|
+
import { createHash } from "crypto";
|
|
794
|
+
import { readdirSync } from "fs";
|
|
795
|
+
import { join } from "path";
|
|
796
|
+
import { loadProjectInfo as loadProjectInfo4 } from "@noir-ai/core";
|
|
797
|
+
import {
|
|
798
|
+
PHASES,
|
|
799
|
+
TASK_CLASSES,
|
|
800
|
+
writeChangelogStub,
|
|
801
|
+
writeDecisionStub
|
|
802
|
+
} from "@noir-ai/workflow";
|
|
779
803
|
var PHASE_SKILL = {
|
|
780
804
|
// C3 curation (2026-08-10): intake+clarify merged into brainstorming;
|
|
781
805
|
// execute → executing-plans; verify → verifying; document → wrap.
|
|
@@ -791,6 +815,24 @@ function skillFor(phase) {
|
|
|
791
815
|
if (typeof phase !== "string") return null;
|
|
792
816
|
return PHASE_SKILL[phase] ?? null;
|
|
793
817
|
}
|
|
818
|
+
function writeDoneArtifacts(opts, taskId) {
|
|
819
|
+
try {
|
|
820
|
+
const project = loadProjectInfo4(process.cwd());
|
|
821
|
+
const conflict = { conflictPolicy: "preserve", interactive: false };
|
|
822
|
+
writeChangelogStub(project.root, `- ${taskId}: completed (verify gate passed)`, conflict);
|
|
823
|
+
const decisionsDir = join(project.root, ".noir", "decisions");
|
|
824
|
+
let nextN = 1;
|
|
825
|
+
try {
|
|
826
|
+
const files = readdirSync(decisionsDir);
|
|
827
|
+
const nums = files.map((f) => Number.parseInt(f.replace(/\D.*$/, ""), 10)).filter((n) => Number.isFinite(n) && n > 0);
|
|
828
|
+
if (nums.length > 0) nextN = Math.max(...nums) + 1;
|
|
829
|
+
} catch {
|
|
830
|
+
}
|
|
831
|
+
writeDecisionStub(project.root, nextN, taskId, conflict);
|
|
832
|
+
} catch {
|
|
833
|
+
tip("document-phase artifacts could not be written (use --no-artifacts to skip)", opts);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
794
836
|
async function fetchStatus(opts, taskId) {
|
|
795
837
|
const args = typeof taskId === "string" && taskId.length > 0 ? { taskId } : {};
|
|
796
838
|
const res = await callDaemonTool(
|
|
@@ -809,17 +851,23 @@ function formatStamp(ms) {
|
|
|
809
851
|
function renderStatusRow(s, opts) {
|
|
810
852
|
const flag = s.degraded === true ? ` ${badge("warn", "degraded")}` : "";
|
|
811
853
|
log(`task status \u2014 ${s.taskId}${flag}`, opts);
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
854
|
+
const rows = [
|
|
855
|
+
{ label: "Task", value: s.taskId },
|
|
856
|
+
{ label: "Phase", value: s.phase },
|
|
857
|
+
{ label: "State", value: s.state },
|
|
858
|
+
{ label: "Mode", value: s.mode }
|
|
859
|
+
];
|
|
860
|
+
if (typeof s.taskClass === "string" && s.taskClass.length > 0) {
|
|
861
|
+
rows.push({ label: "Class", value: s.taskClass });
|
|
862
|
+
}
|
|
863
|
+
if (typeof s.blockReason === "string" && s.blockReason.length > 0) {
|
|
864
|
+
rows.push({ label: "Block reason", value: s.blockReason });
|
|
865
|
+
}
|
|
866
|
+
rows.push(
|
|
867
|
+
{ label: "Next gate", value: s.nextGate ?? "\u2014" },
|
|
868
|
+
{ label: "Updated", value: formatStamp(s.updatedAt) }
|
|
822
869
|
);
|
|
870
|
+
definitionList(rows, opts);
|
|
823
871
|
}
|
|
824
872
|
var GATE_PHASES = ["spec", "plan", "verify"];
|
|
825
873
|
function nextGateAfter(phase) {
|
|
@@ -912,13 +960,25 @@ async function taskNew(opts) {
|
|
|
912
960
|
}
|
|
913
961
|
mode = opts.mode;
|
|
914
962
|
}
|
|
963
|
+
let taskClass;
|
|
964
|
+
if (opts.taskClass !== void 0) {
|
|
965
|
+
if (!TASK_CLASSES.includes(opts.taskClass)) {
|
|
966
|
+
fail(
|
|
967
|
+
EXIT.USAGE,
|
|
968
|
+
`task new: invalid class '${opts.taskClass}' (expected one of: ${TASK_CLASSES.join(", ")})`,
|
|
969
|
+
opts
|
|
970
|
+
);
|
|
971
|
+
}
|
|
972
|
+
taskClass = opts.taskClass;
|
|
973
|
+
}
|
|
915
974
|
const res = await callDaemonTool(
|
|
916
975
|
opts,
|
|
917
976
|
"workflow_start",
|
|
918
977
|
{
|
|
919
978
|
taskId: opts.slug,
|
|
920
979
|
slug: opts.slug,
|
|
921
|
-
...mode === void 0 ? {} : { mode }
|
|
980
|
+
...mode === void 0 ? {} : { mode },
|
|
981
|
+
...taskClass === void 0 ? {} : { taskClass }
|
|
922
982
|
}
|
|
923
983
|
);
|
|
924
984
|
if (res.ok !== true) {
|
|
@@ -952,6 +1012,9 @@ async function taskAdvance(opts) {
|
|
|
952
1012
|
const detail = typeof res.error === "string" && res.error.length > 0 ? res.error : "advance failed";
|
|
953
1013
|
fail(EXIT.ERROR, `task advance: ${detail}`, opts);
|
|
954
1014
|
}
|
|
1015
|
+
if (res.state === "done" && opts.noArtifacts !== true) {
|
|
1016
|
+
writeDoneArtifacts(opts, res.taskId);
|
|
1017
|
+
}
|
|
955
1018
|
if (typeof opts.to === "string" && opts.to === "verify") {
|
|
956
1019
|
tip("run `noir handoff` for a ready-to-paste host prompt", opts);
|
|
957
1020
|
}
|
|
@@ -962,6 +1025,272 @@ async function taskAdvance(opts) {
|
|
|
962
1025
|
}
|
|
963
1026
|
renderStatusRow(res, opts);
|
|
964
1027
|
}
|
|
1028
|
+
function renderResumeBriefing(r, opts) {
|
|
1029
|
+
const flag = r.degraded === true ? ` ${badge("warn", "degraded")}` : "";
|
|
1030
|
+
log(`resume \u2014 ${r.taskId} (${r.state}, ${r.mode})${flag}`, opts);
|
|
1031
|
+
const rows = [
|
|
1032
|
+
{ label: "Task", value: String(r.taskId) },
|
|
1033
|
+
{ label: "Phase", value: String(r.phase) },
|
|
1034
|
+
{ label: "State", value: String(r.state) },
|
|
1035
|
+
{ label: "Mode", value: String(r.mode) }
|
|
1036
|
+
];
|
|
1037
|
+
if (typeof r.taskClass === "string" && r.taskClass.length > 0) {
|
|
1038
|
+
rows.push({ label: "Class", value: r.taskClass });
|
|
1039
|
+
}
|
|
1040
|
+
if (typeof r.blockReason === "string" && r.blockReason.length > 0) {
|
|
1041
|
+
rows.push({ label: "Block reason", value: r.blockReason });
|
|
1042
|
+
}
|
|
1043
|
+
rows.push({ label: "Next gate", value: r.nextGate ?? "\u2014" });
|
|
1044
|
+
definitionList(rows, opts);
|
|
1045
|
+
const suggestion = skillFor(r.phase ?? null);
|
|
1046
|
+
if (suggestion) info(`skill for current phase (${r.phase}): ${suggestion}`, opts);
|
|
1047
|
+
if (r.state === "blocked") {
|
|
1048
|
+
info("resume a blocked task with: noir task advance --to <phase>", opts);
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
async function taskResume(opts) {
|
|
1052
|
+
const args = {};
|
|
1053
|
+
if (typeof opts.id === "string" && opts.id.length > 0) {
|
|
1054
|
+
args.taskId = opts.id;
|
|
1055
|
+
}
|
|
1056
|
+
const res = await callDaemonTool(opts, "workflow_resume", args);
|
|
1057
|
+
if (res.resumable !== true) {
|
|
1058
|
+
const detail = typeof res.error === "string" && res.error.length > 0 ? res.error : "nothing to resume";
|
|
1059
|
+
if (opts.json === true) {
|
|
1060
|
+
process.stdout.write(`${JSON.stringify({ ok: false, resumable: false, error: detail })}
|
|
1061
|
+
`);
|
|
1062
|
+
return;
|
|
1063
|
+
}
|
|
1064
|
+
fail(EXIT.ERROR, `task resume: ${detail}`, opts);
|
|
1065
|
+
}
|
|
1066
|
+
if (typeof opts.prompt === "string" && opts.prompt.length > 0) {
|
|
1067
|
+
info(`resume prompt: ${opts.prompt}`, opts);
|
|
1068
|
+
}
|
|
1069
|
+
if (opts.json === true) {
|
|
1070
|
+
process.stdout.write(`${JSON.stringify({ ok: true, data: res })}
|
|
1071
|
+
`);
|
|
1072
|
+
return;
|
|
1073
|
+
}
|
|
1074
|
+
renderResumeBriefing(res, opts);
|
|
1075
|
+
}
|
|
1076
|
+
async function taskBlock(opts) {
|
|
1077
|
+
if (typeof opts.reason !== "string" || opts.reason.trim().length === 0) {
|
|
1078
|
+
fail(EXIT.USAGE, "task block: a non-empty <reason> is required", opts);
|
|
1079
|
+
}
|
|
1080
|
+
const args = { reason: opts.reason };
|
|
1081
|
+
if (typeof opts.task === "string" && opts.task.length > 0) args.taskId = opts.task;
|
|
1082
|
+
const res = await callDaemonTool(
|
|
1083
|
+
opts,
|
|
1084
|
+
"workflow_block",
|
|
1085
|
+
args
|
|
1086
|
+
);
|
|
1087
|
+
if (res.ok !== true) {
|
|
1088
|
+
const detail = typeof res.error === "string" && res.error.length > 0 ? res.error : "block failed";
|
|
1089
|
+
fail(EXIT.ERROR, `task block: ${detail}`, opts);
|
|
1090
|
+
}
|
|
1091
|
+
if (opts.json === true) {
|
|
1092
|
+
process.stdout.write(`${JSON.stringify({ ok: true, data: res })}
|
|
1093
|
+
`);
|
|
1094
|
+
return;
|
|
1095
|
+
}
|
|
1096
|
+
renderStatusRow(res, opts);
|
|
1097
|
+
}
|
|
1098
|
+
async function taskAbandon(opts) {
|
|
1099
|
+
if (isInteractive(opts)) {
|
|
1100
|
+
const clack = await import("@clack/prompts");
|
|
1101
|
+
const confirm = await clack.confirm({
|
|
1102
|
+
message: "Abandon this task? This is terminal and cannot be undone.",
|
|
1103
|
+
initialValue: false
|
|
1104
|
+
});
|
|
1105
|
+
if (clack.isCancel(confirm)) {
|
|
1106
|
+
clack.cancel("Cancelled.");
|
|
1107
|
+
fail(EXIT.CANCELLED, "cancelled", opts);
|
|
1108
|
+
}
|
|
1109
|
+
if (confirm !== true) {
|
|
1110
|
+
fail(EXIT.CANCELLED, "abandon cancelled", opts);
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
const args = {};
|
|
1114
|
+
if (typeof opts.task === "string" && opts.task.length > 0) args.taskId = opts.task;
|
|
1115
|
+
const res = await callDaemonTool(
|
|
1116
|
+
opts,
|
|
1117
|
+
"workflow_abandon",
|
|
1118
|
+
args
|
|
1119
|
+
);
|
|
1120
|
+
if (res.ok !== true) {
|
|
1121
|
+
const detail = typeof res.error === "string" && res.error.length > 0 ? res.error : "abandon failed";
|
|
1122
|
+
fail(EXIT.ERROR, `task abandon: ${detail}`, opts);
|
|
1123
|
+
}
|
|
1124
|
+
if (opts.json === true) {
|
|
1125
|
+
process.stdout.write(`${JSON.stringify({ ok: true, data: res })}
|
|
1126
|
+
`);
|
|
1127
|
+
return;
|
|
1128
|
+
}
|
|
1129
|
+
renderStatusRow(res, opts);
|
|
1130
|
+
}
|
|
1131
|
+
function runCheck(cmd) {
|
|
1132
|
+
try {
|
|
1133
|
+
const buf = spawnSync("sh", ["-c", cmd], {
|
|
1134
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1135
|
+
encoding: "utf8"
|
|
1136
|
+
});
|
|
1137
|
+
let out = "";
|
|
1138
|
+
if (buf.stdout) out += buf.stdout.toString();
|
|
1139
|
+
if (buf.stderr) out += buf.stderr.toString();
|
|
1140
|
+
const status2 = buf.status;
|
|
1141
|
+
return {
|
|
1142
|
+
exitCode: typeof status2 === "number" ? status2 : 1,
|
|
1143
|
+
digest: createHash("sha256").update(out).digest("hex")
|
|
1144
|
+
};
|
|
1145
|
+
} catch {
|
|
1146
|
+
return { exitCode: 1, digest: createHash("sha256").update("").digest("hex") };
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
async function taskVerify(opts) {
|
|
1150
|
+
const project = loadProjectInfo4(process.cwd());
|
|
1151
|
+
const cfgChecks = project.config.workflow?.gate?.verify?.checks ?? [];
|
|
1152
|
+
const all = cfgChecks.map((c2) => ({
|
|
1153
|
+
name: c2.name,
|
|
1154
|
+
command: c2.command,
|
|
1155
|
+
tier: c2.tier ?? "hard"
|
|
1156
|
+
}));
|
|
1157
|
+
let checks = all;
|
|
1158
|
+
if (opts.check && opts.check.length > 0) {
|
|
1159
|
+
const want = new Set(opts.check);
|
|
1160
|
+
checks = all.filter((c2) => want.has(c2.name));
|
|
1161
|
+
if (checks.length === 0) {
|
|
1162
|
+
fail(
|
|
1163
|
+
EXIT.USAGE,
|
|
1164
|
+
`task verify: no configured checks match --check ${[...want].join(",")}`,
|
|
1165
|
+
opts
|
|
1166
|
+
);
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
if (checks.length === 0) {
|
|
1170
|
+
fail(
|
|
1171
|
+
EXIT.USAGE,
|
|
1172
|
+
"task verify: no verify checks configured (set workflow.gate.verify.checks in noir.config)",
|
|
1173
|
+
opts
|
|
1174
|
+
);
|
|
1175
|
+
}
|
|
1176
|
+
const ranAt = Date.now();
|
|
1177
|
+
const evidence = {
|
|
1178
|
+
ranAt,
|
|
1179
|
+
checks: checks.map((c2) => {
|
|
1180
|
+
const r = runCheck(c2.command);
|
|
1181
|
+
return {
|
|
1182
|
+
name: c2.name,
|
|
1183
|
+
exitCode: r.exitCode,
|
|
1184
|
+
outputDigest: r.digest,
|
|
1185
|
+
command: c2.command,
|
|
1186
|
+
tier: c2.tier
|
|
1187
|
+
};
|
|
1188
|
+
}),
|
|
1189
|
+
summary: ""
|
|
1190
|
+
};
|
|
1191
|
+
const passed = evidence.checks.filter((c2) => c2.exitCode === 0).length;
|
|
1192
|
+
const failed = evidence.checks.length - passed;
|
|
1193
|
+
evidence.summary = `${passed} passed, ${failed} failed`;
|
|
1194
|
+
const res = await callDaemonTool(opts, "workflow_advance", { evidence });
|
|
1195
|
+
if (res.ok === true) {
|
|
1196
|
+
if (opts.json === true) {
|
|
1197
|
+
process.stdout.write(`${JSON.stringify({ ok: true, data: res, evidence })}
|
|
1198
|
+
`);
|
|
1199
|
+
return;
|
|
1200
|
+
}
|
|
1201
|
+
success(`verify \u2014 ${evidence.summary} \u2192 gate approved`, opts);
|
|
1202
|
+
renderStatusRow(res, opts);
|
|
1203
|
+
return;
|
|
1204
|
+
}
|
|
1205
|
+
if (opts.json === true) {
|
|
1206
|
+
process.stdout.write(`${JSON.stringify({ evidence, ...res })}
|
|
1207
|
+
`);
|
|
1208
|
+
return;
|
|
1209
|
+
}
|
|
1210
|
+
if (res.pendingGate) {
|
|
1211
|
+
warn(`verify gate: ${res.pendingGate.reason} (${evidence.summary})`, opts);
|
|
1212
|
+
const recovery = res.recovery ?? ["retry", "force", "skip", "block"];
|
|
1213
|
+
info(
|
|
1214
|
+
`recovery: ${recovery.map(
|
|
1215
|
+
(r) => r === "retry" ? "`noir task verify`" : r === "force" ? "`noir task advance --force <reason>`" : r === "skip" ? "`noir task advance --skip`" : r === "block" ? "`noir task block <reason>`" : `\`${r}\``
|
|
1216
|
+
).join(" | ")}`,
|
|
1217
|
+
opts
|
|
1218
|
+
);
|
|
1219
|
+
} else {
|
|
1220
|
+
fail(EXIT.ERROR, `task verify: ${res.error ?? "verify failed"}`, opts);
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
async function taskResearch(opts) {
|
|
1224
|
+
const s = await fetchStatus(opts, opts.id);
|
|
1225
|
+
if (opts.json === true) {
|
|
1226
|
+
process.stdout.write(`${JSON.stringify({ ok: true, data: s })}
|
|
1227
|
+
`);
|
|
1228
|
+
return;
|
|
1229
|
+
}
|
|
1230
|
+
renderStatusRow(s, opts);
|
|
1231
|
+
info("research findings are recorded via `noir task research record` (list + record)", opts);
|
|
1232
|
+
}
|
|
1233
|
+
async function taskResearchRecord(opts) {
|
|
1234
|
+
const TYPES = ["assumption", "discovery", "decision", "grounding-fact"];
|
|
1235
|
+
if (!TYPES.includes(opts.type)) {
|
|
1236
|
+
fail(
|
|
1237
|
+
EXIT.USAGE,
|
|
1238
|
+
`task research record: invalid type '${opts.type}' (expected one of: ${TYPES.join(", ")})`,
|
|
1239
|
+
opts
|
|
1240
|
+
);
|
|
1241
|
+
}
|
|
1242
|
+
const args = { type: opts.type, text: opts.text };
|
|
1243
|
+
if (typeof opts.source === "string" && opts.source.length > 0) args.source = opts.source;
|
|
1244
|
+
if (typeof opts.task === "string" && opts.task.length > 0) args.taskId = opts.task;
|
|
1245
|
+
const res = await callDaemonTool(opts, "workflow_research_record", args);
|
|
1246
|
+
if (res.ok !== true) {
|
|
1247
|
+
const detail = typeof res.error === "string" && res.error.length > 0 ? res.error : "record failed";
|
|
1248
|
+
fail(EXIT.ERROR, `task research record: ${detail}`, opts);
|
|
1249
|
+
}
|
|
1250
|
+
if (opts.json === true) {
|
|
1251
|
+
process.stdout.write(`${JSON.stringify({ ok: true, data: res })}
|
|
1252
|
+
`);
|
|
1253
|
+
return;
|
|
1254
|
+
}
|
|
1255
|
+
success(
|
|
1256
|
+
`research recorded \u2192 ${opts.type}: ${opts.text.slice(0, 80)}${opts.text.length > 80 ? "\u2026" : ""}`,
|
|
1257
|
+
opts
|
|
1258
|
+
);
|
|
1259
|
+
}
|
|
1260
|
+
async function taskDecompose(opts) {
|
|
1261
|
+
const cap = opts.capability.trim();
|
|
1262
|
+
if (cap.length === 0) fail(EXIT.USAGE, "task decompose: <capability-id> is required", opts);
|
|
1263
|
+
const plan = {
|
|
1264
|
+
capabilityId: cap,
|
|
1265
|
+
intent: `Capability ${cap} \u2014 details TBD (run with a provider for an LLM-drafted plan)`,
|
|
1266
|
+
slices: [
|
|
1267
|
+
{
|
|
1268
|
+
id: "s1-walking-skeleton",
|
|
1269
|
+
title: "Walking skeleton \u2014 thinnest end-to-end",
|
|
1270
|
+
type: "feature",
|
|
1271
|
+
rationale: `The first slice for capability ${cap}.`,
|
|
1272
|
+
scopeIn: "End-to-end path linking the main architecture.",
|
|
1273
|
+
scopeOut: "Polish, edge cases, full coverage.",
|
|
1274
|
+
dependsOn: [],
|
|
1275
|
+
files: { create: [], modify: [], preserve: [] },
|
|
1276
|
+
acceptance: [`verify acceptance criteria for capability ${cap}`],
|
|
1277
|
+
doD: ["pnpm test", "pnpm typecheck"],
|
|
1278
|
+
rollbackPlan: { procedure: "revert the slice commit", verifyCommand: "pnpm test" }
|
|
1279
|
+
}
|
|
1280
|
+
],
|
|
1281
|
+
status: { "s1-walking-skeleton": "planned" }
|
|
1282
|
+
};
|
|
1283
|
+
if (opts.json === true) {
|
|
1284
|
+
process.stdout.write(`${JSON.stringify({ ok: true, data: plan })}
|
|
1285
|
+
`);
|
|
1286
|
+
return;
|
|
1287
|
+
}
|
|
1288
|
+
log(`decompose \u2014 ${cap}`, opts);
|
|
1289
|
+
for (const s of plan.slices) {
|
|
1290
|
+
info(`${s.id}: ${s.title} (${s.type}) \u2014 rationale: ${s.rationale}`, opts);
|
|
1291
|
+
}
|
|
1292
|
+
tip("each slice enters the existing clarify\u2192spec\u2192plan\u2192execute\u2192verify\u2192document FSM cycle", opts);
|
|
1293
|
+
}
|
|
965
1294
|
|
|
966
1295
|
// src/commands/handoff.ts
|
|
967
1296
|
var SEED_LIMIT = 5;
|
|
@@ -1069,7 +1398,7 @@ function resolveDirective(root, p) {
|
|
|
1069
1398
|
}
|
|
1070
1399
|
function handoffFilePath(root, taskId, projectId) {
|
|
1071
1400
|
const id = typeof taskId === "string" && taskId.length > 0 ? taskId : projectId;
|
|
1072
|
-
return
|
|
1401
|
+
return join2(root, NOIR_DIR, "handoff", `${id}.md`);
|
|
1073
1402
|
}
|
|
1074
1403
|
async function extractSeeds(opts, probe, query) {
|
|
1075
1404
|
if (!probe.running) return { context: null, memory: null };
|
|
@@ -1101,7 +1430,7 @@ async function probeOnly(opts) {
|
|
|
1101
1430
|
return probeDaemon(opts);
|
|
1102
1431
|
}
|
|
1103
1432
|
async function handoff(opts) {
|
|
1104
|
-
const project =
|
|
1433
|
+
const project = loadProjectInfo5(process.cwd());
|
|
1105
1434
|
const host = project.config.host;
|
|
1106
1435
|
const snapshot = await gatherStatusPayload(opts);
|
|
1107
1436
|
const probe = await probeOnly(opts);
|
|
@@ -1135,7 +1464,7 @@ async function handoff(opts) {
|
|
|
1135
1464
|
const md = renderMarkdown(full);
|
|
1136
1465
|
if (opts.write === true) {
|
|
1137
1466
|
const path = handoffFilePath(project.root, task?.taskId, project.id);
|
|
1138
|
-
mkdirSync(
|
|
1467
|
+
mkdirSync(join2(project.root, NOIR_DIR, "handoff"), { recursive: true });
|
|
1139
1468
|
writeFileSync(path, md, "utf8");
|
|
1140
1469
|
info(`handoff written to ${path}`, opts);
|
|
1141
1470
|
return;
|
|
@@ -1146,7 +1475,7 @@ async function handoff(opts) {
|
|
|
1146
1475
|
// src/commands/home.ts
|
|
1147
1476
|
import { hostLaunchDirective as hostLaunchDirective2 } from "@noir-ai/adapters";
|
|
1148
1477
|
import {
|
|
1149
|
-
loadProjectInfo as
|
|
1478
|
+
loadProjectInfo as loadProjectInfo6,
|
|
1150
1479
|
NOIR_VERSION as NOIR_VERSION3,
|
|
1151
1480
|
readInstallRecord
|
|
1152
1481
|
} from "@noir-ai/core";
|
|
@@ -1444,7 +1773,7 @@ async function resolveSections(commands) {
|
|
|
1444
1773
|
// src/commands/home.ts
|
|
1445
1774
|
function tryProject() {
|
|
1446
1775
|
try {
|
|
1447
|
-
const info2 =
|
|
1776
|
+
const info2 = loadProjectInfo6(process.cwd());
|
|
1448
1777
|
return { id: info2.id, name: info2.name, host: info2.config.host };
|
|
1449
1778
|
} catch {
|
|
1450
1779
|
return null;
|
|
@@ -1608,7 +1937,7 @@ async function runMenu(opts, deps) {
|
|
|
1608
1937
|
env: process.env,
|
|
1609
1938
|
configUpdate: (() => {
|
|
1610
1939
|
try {
|
|
1611
|
-
const info2 =
|
|
1940
|
+
const info2 = loadProjectInfo6(process.cwd());
|
|
1612
1941
|
return info2.config.update ?? DEFAULT_UPDATE_CONFIG;
|
|
1613
1942
|
} catch {
|
|
1614
1943
|
return DEFAULT_UPDATE_CONFIG;
|
|
@@ -1904,9 +2233,105 @@ async function memoryConsolidate(opts) {
|
|
|
1904
2233
|
fail(EXIT.ERROR, `memory consolidate did not run: ${reason}${suffix}`, opts);
|
|
1905
2234
|
}
|
|
1906
2235
|
|
|
2236
|
+
// src/commands/release.ts
|
|
2237
|
+
import { execSync, spawnSync as spawnSync2 } from "child_process";
|
|
2238
|
+
function describe(step, who) {
|
|
2239
|
+
const m = {
|
|
2240
|
+
preflight: "check clean tree, on develop, HEAD pushed, gate green, no version collision",
|
|
2241
|
+
bump: "run bump-version.mjs + update CHANGELOG + roadmap docs",
|
|
2242
|
+
gate: "re-run the full gate (lint/build/typecheck/test/docs:validate)",
|
|
2243
|
+
commit: "commit chore(release) + push develop",
|
|
2244
|
+
"ci-develop": "wait for CI on develop \u2192 green",
|
|
2245
|
+
"beta-tag": "pnpm release:tag \u2192 beta tag + push",
|
|
2246
|
+
"human-approve-beta": "approve the release workflow publish job in GitHub Actions",
|
|
2247
|
+
"merge-main": "merge develop \u2192 main (fast-forward) + push",
|
|
2248
|
+
"ci-main": "wait for CI on main \u2192 green",
|
|
2249
|
+
"stable-tag": "pnpm release:tag \u2192 stable tag + push",
|
|
2250
|
+
"human-approve-stable": "approve the release workflow publish job in GitHub Actions (again)",
|
|
2251
|
+
"dist-bump": "update Homebrew (packaging/homebrew/noir.rb) + Scoop (packaging/scoop/noir.json) from npm",
|
|
2252
|
+
sync: "merge main \u2192 develop + verify both branches at same SHA"
|
|
2253
|
+
};
|
|
2254
|
+
return `${m[step]} [${who}]`;
|
|
2255
|
+
}
|
|
2256
|
+
var ALL = [
|
|
2257
|
+
"preflight",
|
|
2258
|
+
"bump",
|
|
2259
|
+
"gate",
|
|
2260
|
+
"commit",
|
|
2261
|
+
"ci-develop",
|
|
2262
|
+
"beta-tag",
|
|
2263
|
+
"human-approve-beta",
|
|
2264
|
+
"merge-main",
|
|
2265
|
+
"ci-main",
|
|
2266
|
+
"stable-tag",
|
|
2267
|
+
"human-approve-stable",
|
|
2268
|
+
"dist-bump",
|
|
2269
|
+
"sync"
|
|
2270
|
+
];
|
|
2271
|
+
async function release(opts) {
|
|
2272
|
+
if (opts.dryRun) {
|
|
2273
|
+
log("noir release --dry-run (no changes will be made)", opts);
|
|
2274
|
+
for (const s of ALL) {
|
|
2275
|
+
const who = s.startsWith("human-approve") ? "human" : "noir";
|
|
2276
|
+
info(`${s}: ${describe(s, who)}`, opts);
|
|
2277
|
+
}
|
|
2278
|
+
tip('full release flow documented in CLAUDE.md "Patch release flow (beta \u2192 stable)"', opts);
|
|
2279
|
+
return;
|
|
2280
|
+
}
|
|
2281
|
+
const version = opts.version?.trim();
|
|
2282
|
+
if (!version) {
|
|
2283
|
+
fail(EXIT.USAGE, "release: <version> is required (e.g. noir release 1.10.0)", opts);
|
|
2284
|
+
}
|
|
2285
|
+
const channel = opts.channel === "stable" ? "stable" : "beta";
|
|
2286
|
+
log(`noir release ${version} (${channel} channel)`, opts);
|
|
2287
|
+
try {
|
|
2288
|
+
info(`[1/13] preflight: ${describe("preflight", "noir")}`, opts);
|
|
2289
|
+
execSync("git diff --quiet", { encoding: "utf8" });
|
|
2290
|
+
const branch = execSync("git branch --show-current", { encoding: "utf8" }).trim();
|
|
2291
|
+
if (branch !== "develop") fail(EXIT.ERROR, "release: must be on develop", opts);
|
|
2292
|
+
const status2 = execSync("git status --porcelain", { encoding: "utf8" }).trim();
|
|
2293
|
+
if (status2) fail(EXIT.ERROR, "release: tree must be clean", opts);
|
|
2294
|
+
success("preflight: \u2713 clean, on develop", opts);
|
|
2295
|
+
info(`[2/13] bump: ${describe("bump", "noir")}`, opts);
|
|
2296
|
+
spawnSync2("node", ["scripts/bump-version.mjs", version], {
|
|
2297
|
+
encoding: "utf8",
|
|
2298
|
+
stdio: "inherit"
|
|
2299
|
+
});
|
|
2300
|
+
success(`bump: \u2713 version ${version}`, opts);
|
|
2301
|
+
info(`[3/13] gate: ${describe("gate", "noir")}`, opts);
|
|
2302
|
+
execSync("pnpm lint && pnpm build && pnpm typecheck && pnpm test && pnpm docs:validate", {
|
|
2303
|
+
encoding: "utf8",
|
|
2304
|
+
stdio: "inherit"
|
|
2305
|
+
});
|
|
2306
|
+
success("gate: \u2713 full gate green", opts);
|
|
2307
|
+
info(`[4/13] commit/push: ${describe("commit", "noir")}`, opts);
|
|
2308
|
+
spawnSync2("git", ["add", "-A"], { encoding: "utf8", stdio: "inherit" });
|
|
2309
|
+
spawnSync2("git", ["commit", "-m", `chore(release): v${version} + docs sync`], {
|
|
2310
|
+
encoding: "utf8",
|
|
2311
|
+
stdio: "inherit"
|
|
2312
|
+
});
|
|
2313
|
+
execSync("git push origin develop", { encoding: "utf8" });
|
|
2314
|
+
success("commit: \u2713 pushed to develop", opts);
|
|
2315
|
+
info(`[5/13] ci-develop: waiting for CI on develop\u2026`, opts);
|
|
2316
|
+
info(`[6/13] beta-tag: ${describe("beta-tag", "noir")}`, opts);
|
|
2317
|
+
tip("after CI green: run `pnpm release:tag` and push the beta tag", opts);
|
|
2318
|
+
warn(`[7/13] ${describe("human-approve-beta", "human")}`, opts);
|
|
2319
|
+
tip("open GitHub Actions \u2192 approve the publish job, then continue", opts);
|
|
2320
|
+
tip("after approval: verify `npm view @noir-ai/cli dist-tags beta` = beta version", opts);
|
|
2321
|
+
info("remaining steps (merge\u2192stable\u2192dist\u2192sync): see CLAUDE.md", opts);
|
|
2322
|
+
for (const s of ALL.slice(7)) {
|
|
2323
|
+
const who = s.startsWith("human-approve") ? "human" : "noir";
|
|
2324
|
+
info(` ${s}: ${describe(s, who)}`, opts);
|
|
2325
|
+
}
|
|
2326
|
+
success(`release ${version}: preflight\u2192bump\u2192gate\u2192commit done. Resume after approval.`, opts);
|
|
2327
|
+
} catch (err) {
|
|
2328
|
+
fail(EXIT.ERROR, `release failed: ${err instanceof Error ? err.message : String(err)}`, opts);
|
|
2329
|
+
}
|
|
2330
|
+
}
|
|
2331
|
+
|
|
1907
2332
|
// src/commands/skills.ts
|
|
1908
2333
|
import { claudeAdapter } from "@noir-ai/adapters";
|
|
1909
|
-
import { loadProjectInfo as
|
|
2334
|
+
import { loadProjectInfo as loadProjectInfo7 } from "@noir-ai/core";
|
|
1910
2335
|
import {
|
|
1911
2336
|
buildRegistry,
|
|
1912
2337
|
discoverAll,
|
|
@@ -2000,7 +2425,7 @@ async function skillsList(opts) {
|
|
|
2000
2425
|
}
|
|
2001
2426
|
async function skillsSync(opts) {
|
|
2002
2427
|
const root = process.cwd();
|
|
2003
|
-
const project =
|
|
2428
|
+
const project = loadProjectInfo7(root);
|
|
2004
2429
|
if (!claudeAdapter.skillsDir) {
|
|
2005
2430
|
const data2 = {
|
|
2006
2431
|
emitted: [],
|
|
@@ -2467,15 +2892,20 @@ function createProgram() {
|
|
|
2467
2892
|
throw new NoirCliError(EXIT.USAGE, "Usage: noir skills list|sync|lint|registry");
|
|
2468
2893
|
});
|
|
2469
2894
|
const taskGrp = program2.command("task").description("workflow task control");
|
|
2470
|
-
taskGrp.command("new").description("start a new workflow task").requiredOption("--slug <slug>", "task slug").option("--mode <mode>", "full | quick").
|
|
2895
|
+
taskGrp.command("new").description("start a new workflow task").requiredOption("--slug <slug>", "task slug").option("--mode <mode>", "full | quick").option(
|
|
2896
|
+
"--class <taskClass>",
|
|
2897
|
+
"task class (feature/epic/enhancement/bugfix/spike/quick-task/refactor) \u2014 drives the PRD gate"
|
|
2898
|
+
).action(async (...args) => {
|
|
2471
2899
|
const cmd = trailingCmd(args);
|
|
2472
2900
|
const g = cmd.optsWithGlobals();
|
|
2473
2901
|
const slug = typeof g.slug === "string" ? g.slug : "";
|
|
2474
2902
|
const mode = typeof g.mode === "string" ? g.mode : void 0;
|
|
2903
|
+
const taskClass = typeof g.class === "string" ? g.class : void 0;
|
|
2475
2904
|
await taskNew({
|
|
2476
2905
|
...toCliOptions(g),
|
|
2477
2906
|
slug,
|
|
2478
|
-
...mode === void 0 ? {} : { mode }
|
|
2907
|
+
...mode === void 0 ? {} : { mode },
|
|
2908
|
+
...taskClass === void 0 ? {} : { taskClass }
|
|
2479
2909
|
});
|
|
2480
2910
|
});
|
|
2481
2911
|
taskGrp.command("status").description("active (or named) task status").argument("[id]", "task id (defaults to active)").action(async (...args) => {
|
|
@@ -2488,7 +2918,7 @@ function createProgram() {
|
|
|
2488
2918
|
...id === void 0 ? {} : { id }
|
|
2489
2919
|
});
|
|
2490
2920
|
});
|
|
2491
|
-
taskGrp.command("advance").description("advance the active task to the next phase").option("--to <phase>", "target phase").option("--force <reason>", "force the gate with a reason").action(async (...args) => {
|
|
2921
|
+
taskGrp.command("advance").description("advance the active task to the next phase").option("--to <phase>", "target phase").option("--force <reason>", "force the gate with a reason").option("--no-artifacts", "skip the document-phase artifact writes at done").action(async (...args) => {
|
|
2492
2922
|
const cmd = trailingCmd(args);
|
|
2493
2923
|
const g = cmd.optsWithGlobals();
|
|
2494
2924
|
const to = typeof g.to === "string" ? g.to : void 0;
|
|
@@ -2496,14 +2926,99 @@ function createProgram() {
|
|
|
2496
2926
|
await taskAdvance({
|
|
2497
2927
|
...toCliOptions(g),
|
|
2498
2928
|
...to === void 0 ? {} : { to },
|
|
2499
|
-
...force === void 0 ? {} : { force }
|
|
2929
|
+
...force === void 0 ? {} : { force },
|
|
2930
|
+
...g.artifacts === false ? { noArtifacts: true } : {}
|
|
2500
2931
|
});
|
|
2501
2932
|
});
|
|
2502
2933
|
taskGrp.command("next").description("suggest the next phase + applicable skill").action(async (...args) => {
|
|
2503
2934
|
await taskNext(toCliOptions(trailingCmd(args).optsWithGlobals()));
|
|
2504
2935
|
});
|
|
2936
|
+
taskGrp.command("decompose").description(
|
|
2937
|
+
"decompose a capability into buildable slices (template-only offline; see the spec)"
|
|
2938
|
+
).argument("<capability>", "capability id, e.g. C4").option("--out <path>", "output path for the SlicePlan JSON").action(async (...args) => {
|
|
2939
|
+
const cmd = trailingCmd(args);
|
|
2940
|
+
const g = cmd.optsWithGlobals();
|
|
2941
|
+
const capability = typeof args[0] === "string" ? args[0] : "";
|
|
2942
|
+
await taskDecompose({
|
|
2943
|
+
...toCliOptions(g),
|
|
2944
|
+
capability,
|
|
2945
|
+
...typeof g.out === "string" && g.out.length > 0 ? { out: g.out } : {}
|
|
2946
|
+
});
|
|
2947
|
+
});
|
|
2948
|
+
taskGrp.command("verify").description("run configured verify checks and submit evidence to the verify gate").option(
|
|
2949
|
+
"--check <name>",
|
|
2950
|
+
"restrict to a named check (repeatable)",
|
|
2951
|
+
(val, acc) => [...acc, val],
|
|
2952
|
+
[]
|
|
2953
|
+
).action(async (...args) => {
|
|
2954
|
+
const cmd = trailingCmd(args);
|
|
2955
|
+
const g = cmd.optsWithGlobals();
|
|
2956
|
+
const check = Array.isArray(g.check) ? g.check : void 0;
|
|
2957
|
+
await taskVerify({
|
|
2958
|
+
...toCliOptions(g),
|
|
2959
|
+
...check === void 0 || check.length === 0 ? {} : { check }
|
|
2960
|
+
});
|
|
2961
|
+
});
|
|
2962
|
+
taskGrp.command("research").description("list research findings for the active (or named) task").argument("[id]", "task id (defaults to active)").action(async (...args) => {
|
|
2963
|
+
const cmd = trailingCmd(args);
|
|
2964
|
+
const g = cmd.optsWithGlobals();
|
|
2965
|
+
const first = args[0];
|
|
2966
|
+
const id = typeof first === "string" ? first : void 0;
|
|
2967
|
+
await taskResearch({
|
|
2968
|
+
...toCliOptions(g),
|
|
2969
|
+
...id === void 0 ? {} : { id }
|
|
2970
|
+
});
|
|
2971
|
+
});
|
|
2972
|
+
taskGrp.command("research-record").description("record a research finding for the active (or named) task").requiredOption("--type <type>", "assumption | discovery | decision | grounding-fact").requiredOption("--text <text>", "finding text (capped)").option("--source <ref>", "evidence/citation (required unless grounding-fact)").option("--task <id>", "task id (defaults to active)").action(async (...args) => {
|
|
2973
|
+
const cmd = trailingCmd(args);
|
|
2974
|
+
const g = cmd.optsWithGlobals();
|
|
2975
|
+
await taskResearchRecord({
|
|
2976
|
+
...toCliOptions(g),
|
|
2977
|
+
type: String(g.type ?? ""),
|
|
2978
|
+
text: String(g.text ?? ""),
|
|
2979
|
+
...typeof g.source === "string" && g.source.length > 0 ? { source: g.source } : {},
|
|
2980
|
+
...typeof g.task === "string" && g.task.length > 0 ? { task: g.task } : {}
|
|
2981
|
+
});
|
|
2982
|
+
});
|
|
2983
|
+
taskGrp.command("resume").description("resume the active (or named) in-flight/blocked task").argument("[id]", "task id (defaults to active)").option("--last", "target the active task explicitly (scripting)").option("--prompt <text>", "a continue instruction to surface in the briefing").action(async (...args) => {
|
|
2984
|
+
const cmd = trailingCmd(args);
|
|
2985
|
+
const g = cmd.optsWithGlobals();
|
|
2986
|
+
const first = args[0];
|
|
2987
|
+
const id = typeof first === "string" ? first : void 0;
|
|
2988
|
+
const last = g.last === true;
|
|
2989
|
+
const prompt = typeof g.prompt === "string" ? g.prompt : void 0;
|
|
2990
|
+
await taskResume({
|
|
2991
|
+
...toCliOptions(g),
|
|
2992
|
+
...id === void 0 ? {} : { id },
|
|
2993
|
+
...last ? { last } : {},
|
|
2994
|
+
...prompt === void 0 ? {} : { prompt }
|
|
2995
|
+
});
|
|
2996
|
+
});
|
|
2997
|
+
taskGrp.command("block").description("mark the active (or named) task blocked with a reason").argument("<reason>", "why the task is stuck (non-empty)").option("--task <id>", "task id (defaults to active)").action(async (...args) => {
|
|
2998
|
+
const cmd = trailingCmd(args);
|
|
2999
|
+
const g = cmd.optsWithGlobals();
|
|
3000
|
+
const reason = typeof args[0] === "string" ? args[0] : "";
|
|
3001
|
+
const task = typeof g.task === "string" ? g.task : void 0;
|
|
3002
|
+
await taskBlock({
|
|
3003
|
+
...toCliOptions(g),
|
|
3004
|
+
reason,
|
|
3005
|
+
...task === void 0 ? {} : { task }
|
|
3006
|
+
});
|
|
3007
|
+
});
|
|
3008
|
+
taskGrp.command("abandon").description("abandon the active (or named) task (terminal, confirmed)").option("--task <id>", "task id (defaults to active)").action(async (...args) => {
|
|
3009
|
+
const cmd = trailingCmd(args);
|
|
3010
|
+
const g = cmd.optsWithGlobals();
|
|
3011
|
+
const task = typeof g.task === "string" ? g.task : void 0;
|
|
3012
|
+
await taskAbandon({
|
|
3013
|
+
...toCliOptions(g),
|
|
3014
|
+
...task === void 0 ? {} : { task }
|
|
3015
|
+
});
|
|
3016
|
+
});
|
|
2505
3017
|
taskGrp.action(() => {
|
|
2506
|
-
throw new NoirCliError(
|
|
3018
|
+
throw new NoirCliError(
|
|
3019
|
+
EXIT.USAGE,
|
|
3020
|
+
"Usage: noir task new|status|advance|next|verify|research|research-record|resume|block|abandon"
|
|
3021
|
+
);
|
|
2507
3022
|
});
|
|
2508
3023
|
const installCmd = program2.command("install").description(
|
|
2509
3024
|
"install Noir via the native managed-Node path (or migrate from another install method)"
|
|
@@ -2538,6 +3053,16 @@ function createProgram() {
|
|
|
2538
3053
|
program2.command("wrap").description("session-end alias for `noir handoff`").option("--write", "persist to .noir/handoff/<id>.md (gitignored)").action(async (...args) => {
|
|
2539
3054
|
await handoff(buildHandoffOptions(trailingCmd(args).optsWithGlobals()));
|
|
2540
3055
|
});
|
|
3056
|
+
program2.command("release").description("guided release orchestrator over the patch-release flow").argument("[version]", "target version, e.g. 1.10.0").option("--channel <channel>", "beta (default) | stable").option("--dry-run", "print the checklist without executing any steps").action(async (...args) => {
|
|
3057
|
+
const cmd = trailingCmd(args);
|
|
3058
|
+
const g = cmd.optsWithGlobals();
|
|
3059
|
+
await release({
|
|
3060
|
+
...toCliOptions(g),
|
|
3061
|
+
...typeof args[0] === "string" && args[0].length > 0 ? { version: args[0] } : {},
|
|
3062
|
+
...typeof g.channel === "string" && g.channel.length > 0 ? { channel: g.channel } : {},
|
|
3063
|
+
...g.dryRun === true ? { dryRun: true } : {}
|
|
3064
|
+
});
|
|
3065
|
+
});
|
|
2541
3066
|
program2.command("tui").description("interactive Ink dashboard (host \xB7 phase \xB7 daemon + /command dispatch)").action(async (...args) => {
|
|
2542
3067
|
const g = actionGlobals(args);
|
|
2543
3068
|
const opts = toCliOptions(g);
|