primitive-admin 1.1.0-alpha.45 → 1.1.0-alpha.47
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/primitive.js +2 -0
- package/dist/bin/primitive.js.map +1 -1
- package/dist/src/commands/collections.js +11 -0
- package/dist/src/commands/collections.js.map +1 -1
- package/dist/src/commands/databases.js +34 -49
- package/dist/src/commands/databases.js.map +1 -1
- package/dist/src/commands/groups.js +9 -4
- package/dist/src/commands/groups.js.map +1 -1
- package/dist/src/commands/metadata.js +150 -0
- package/dist/src/commands/metadata.js.map +1 -1
- package/dist/src/commands/sync.d.ts +135 -0
- package/dist/src/commands/sync.js +582 -0
- package/dist/src/commands/sync.js.map +1 -1
- package/dist/src/commands/vars.d.ts +8 -0
- package/dist/src/commands/vars.js +110 -0
- package/dist/src/commands/vars.js.map +1 -0
- package/dist/src/commands/webhooks.js +10 -1
- package/dist/src/commands/webhooks.js.map +1 -1
- package/dist/src/commands/workflows.d.ts +30 -0
- package/dist/src/commands/workflows.js +296 -84
- package/dist/src/commands/workflows.js.map +1 -1
- package/dist/src/lib/api-client.d.ts +39 -1
- package/dist/src/lib/api-client.js +56 -2
- package/dist/src/lib/api-client.js.map +1 -1
- package/dist/src/lib/codegen-shared/resolveCodegenSourceDir.d.ts +68 -0
- package/dist/src/lib/codegen-shared/resolveCodegenSourceDir.js +168 -0
- package/dist/src/lib/codegen-shared/resolveCodegenSourceDir.js.map +1 -0
- package/dist/src/lib/generated-allowlist.js +12 -0
- package/dist/src/lib/generated-allowlist.js.map +1 -1
- package/dist/src/lib/toml-metadata-config.d.ts +10 -2
- package/dist/src/lib/toml-metadata-config.js +51 -15
- package/dist/src/lib/toml-metadata-config.js.map +1 -1
- package/dist/src/lib/workflow-codegen/generated-schema-descriptor.d.ts +33 -0
- package/dist/src/lib/workflow-codegen/generated-schema-descriptor.js +158 -1
- package/dist/src/lib/workflow-codegen/generated-schema-descriptor.js.map +1 -1
- package/dist/src/lib/workflow-codegen/generator.d.ts +6 -2
- package/dist/src/lib/workflow-codegen/generator.js +35 -6
- package/dist/src/lib/workflow-codegen/generator.js.map +1 -1
- package/dist/src/lib/workflow-codegen/schemaToTs.d.ts +13 -4
- package/dist/src/lib/workflow-codegen/schemaToTs.js +49 -4
- package/dist/src/lib/workflow-codegen/schemaToTs.js.map +1 -1
- package/dist/src/types/index.d.ts +4 -0
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readFileSync, writeFileSync, statSync,
|
|
1
|
+
import { readFileSync, writeFileSync, statSync, readdirSync, } from "fs";
|
|
2
2
|
import { basename } from "path";
|
|
3
3
|
import * as path from "path";
|
|
4
4
|
import { parseConfigToml, stringifyConfigToml } from "../lib/config-toml.js";
|
|
@@ -6,6 +6,7 @@ import { lookup as mimeLookup } from "mime-types";
|
|
|
6
6
|
import { generateWorkflowTypes, } from "../lib/workflow-codegen/generator.js";
|
|
7
7
|
import { ApiClient } from "../lib/api-client.js";
|
|
8
8
|
import { resolveAppId } from "../lib/config.js";
|
|
9
|
+
import { resolveCodegenSourceDir } from "../lib/codegen-shared/resolveCodegenSourceDir.js";
|
|
9
10
|
import { validateWorkflowToml, formatWorkflowTomlErrors, } from "../lib/workflow-toml-validator.js";
|
|
10
11
|
import { expandWorkflow } from "../lib/workflow-fragments.js";
|
|
11
12
|
import { buildWorkflowPayloadFromToml } from "../lib/workflow-payload.js";
|
|
@@ -92,6 +93,75 @@ export function renderRunStatusStepResults(stepResults) {
|
|
|
92
93
|
}
|
|
93
94
|
return lines;
|
|
94
95
|
}
|
|
96
|
+
/**
|
|
97
|
+
* #1367 — annotate step-run rows with the inter-step gap: the wall-clock time
|
|
98
|
+
* between the previous step's `endedAt` and this step's `startedAt`. Sorts by
|
|
99
|
+
* `startedAt` when every row has a parseable one (tie-break `stepIndex`),
|
|
100
|
+
* otherwise by `stepIndex` for the whole list — the ordering mode must be
|
|
101
|
+
* chosen once for all rows, not per pair: a per-pair timestamp-vs-index
|
|
102
|
+
* fallback is not transitive when some rows lack a timestamp (A<B by index,
|
|
103
|
+
* B<C by index, C<A by time), and a non-transitive comparator leaves
|
|
104
|
+
* Array.prototype.sort free to return an arbitrary order, computing gaps
|
|
105
|
+
* against the wrong predecessor. The first step has no predecessor, so its
|
|
106
|
+
* `gapMs` is `null`. A gap is only meaningful when both adjacent timestamps
|
|
107
|
+
* parse and the gap is positive. When the immediate predecessor lacks a valid
|
|
108
|
+
* `endedAt`, the gap is left `null` rather than measured from an older,
|
|
109
|
+
* non-adjacent step — otherwise a later step could raise a false stall
|
|
110
|
+
* warning against a step it does not actually follow. Exported for
|
|
111
|
+
* testability.
|
|
112
|
+
*/
|
|
113
|
+
export function annotateStepGaps(steps) {
|
|
114
|
+
const rows = [...(steps || [])];
|
|
115
|
+
const allTimed = rows.every((r) => r?.startedAt && !Number.isNaN(new Date(r.startedAt).getTime()));
|
|
116
|
+
rows.sort((a, b) => {
|
|
117
|
+
if (allTimed) {
|
|
118
|
+
const sa = new Date(a.startedAt).getTime();
|
|
119
|
+
const sb = new Date(b.startedAt).getTime();
|
|
120
|
+
if (sa !== sb)
|
|
121
|
+
return sa - sb;
|
|
122
|
+
}
|
|
123
|
+
return (a?.stepIndex ?? 0) - (b?.stepIndex ?? 0);
|
|
124
|
+
});
|
|
125
|
+
let prevEnded = null;
|
|
126
|
+
for (const row of rows) {
|
|
127
|
+
const started = row?.startedAt ? new Date(row.startedAt).getTime() : NaN;
|
|
128
|
+
if (prevEnded != null && !Number.isNaN(started)) {
|
|
129
|
+
const gap = started - prevEnded;
|
|
130
|
+
row.gapMs = gap > 0 ? gap : 0;
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
row.gapMs = null;
|
|
134
|
+
}
|
|
135
|
+
// Only genuinely adjacent steps yield a gap: if this step lacks a valid
|
|
136
|
+
// `endedAt`, clear the predecessor so the NEXT step's gap is left null
|
|
137
|
+
// instead of being measured from a non-adjacent earlier step.
|
|
138
|
+
const ended = row?.endedAt ? new Date(row.endedAt).getTime() : NaN;
|
|
139
|
+
prevEnded = Number.isNaN(ended) ? null : ended;
|
|
140
|
+
}
|
|
141
|
+
return rows;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* #1367 — compute min / p50 / p95 / max over a set of latency samples (ms).
|
|
145
|
+
* Ignores non-finite values. Returns `null` when there are no valid samples.
|
|
146
|
+
* Percentiles use nearest-rank on the sorted samples. Exported for testability.
|
|
147
|
+
*/
|
|
148
|
+
export function summarizeLatencyDistribution(values) {
|
|
149
|
+
const nums = (values || []).filter((v) => typeof v === "number" && Number.isFinite(v));
|
|
150
|
+
if (nums.length === 0)
|
|
151
|
+
return null;
|
|
152
|
+
nums.sort((a, b) => a - b);
|
|
153
|
+
const pct = (p) => {
|
|
154
|
+
const rank = Math.ceil((p / 100) * nums.length);
|
|
155
|
+
return nums[Math.min(nums.length - 1, Math.max(0, rank - 1))];
|
|
156
|
+
};
|
|
157
|
+
return {
|
|
158
|
+
count: nums.length,
|
|
159
|
+
min: nums[0],
|
|
160
|
+
p50: pct(50),
|
|
161
|
+
p95: pct(95),
|
|
162
|
+
max: nums[nums.length - 1],
|
|
163
|
+
};
|
|
164
|
+
}
|
|
95
165
|
/**
|
|
96
166
|
* Issue #687 (review feedback): render an unambiguous label for a workflow
|
|
97
167
|
* config slot ("active config", "draft config", etc.) when the slot may be
|
|
@@ -837,6 +907,188 @@ Examples:
|
|
|
837
907
|
process.exit(1);
|
|
838
908
|
}
|
|
839
909
|
});
|
|
910
|
+
// #1367 — hidden queue-latency measurement tool. Fires a batch of runs of a
|
|
911
|
+
// (trivial, no-op) workflow, polls until each has an executionStartedAt, and
|
|
912
|
+
// reports the observed queueDelayMs / createCallDurationMs distribution. This
|
|
913
|
+
// is the post-deploy verification vehicle: local miniflare does not exhibit
|
|
914
|
+
// real Cloudflare scheduling delay, so run it against an agents/alpha scratch
|
|
915
|
+
// app. Creates are paced (default 10/s) to stay clear of CF's ~100/s shared
|
|
916
|
+
// instance-creation limit; a create failure (e.g. 429) is recorded, not fatal.
|
|
917
|
+
workflows
|
|
918
|
+
.command("queue-latency-test", { hidden: true })
|
|
919
|
+
.description("(hidden) Fire N no-op runs and report the queue-delay distribution (post-deploy tool)")
|
|
920
|
+
.argument("<workflow-id>", "Workflow ID or key of a trivial no-op workflow")
|
|
921
|
+
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
922
|
+
.option("--count <n>", "Number of runs to fire", "50")
|
|
923
|
+
.option("--rate <r>", "Max creates per second (<=10 recommended)", "10")
|
|
924
|
+
.option("--input <json>", "Root input as JSON")
|
|
925
|
+
.option("--timeout <sec>", "Seconds to wait for all runs to start", "120")
|
|
926
|
+
.option("--verbose", "Print a per-run row")
|
|
927
|
+
.option("--json", "Output as JSON")
|
|
928
|
+
.action(async (workflowId, options) => {
|
|
929
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
930
|
+
const client = new ApiClient();
|
|
931
|
+
const count = Math.max(1, parseInt(options.count, 10) || 50);
|
|
932
|
+
const rate = Math.max(1, parseInt(options.rate, 10) || 10);
|
|
933
|
+
const timeoutSec = Math.max(1, parseInt(options.timeout, 10) || 120);
|
|
934
|
+
const intervalMs = Math.ceil(1000 / rate);
|
|
935
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
936
|
+
let rootInput;
|
|
937
|
+
if (options.input) {
|
|
938
|
+
try {
|
|
939
|
+
rootInput = JSON.parse(options.input);
|
|
940
|
+
}
|
|
941
|
+
catch {
|
|
942
|
+
error("Invalid JSON in --input");
|
|
943
|
+
process.exit(1);
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
const fired = [];
|
|
947
|
+
if (!options.json) {
|
|
948
|
+
info(`Firing ${count} run(s) of ${workflowId} at up to ${rate}/s (interval ${intervalMs}ms)...`);
|
|
949
|
+
}
|
|
950
|
+
for (let i = 0; i < count; i++) {
|
|
951
|
+
try {
|
|
952
|
+
const res = await client.previewWorkflow(resolvedAppId, workflowId, {
|
|
953
|
+
rootInput,
|
|
954
|
+
});
|
|
955
|
+
fired.push({
|
|
956
|
+
index: i,
|
|
957
|
+
instanceId: res?.instanceId,
|
|
958
|
+
createFailed: false,
|
|
959
|
+
started: false,
|
|
960
|
+
});
|
|
961
|
+
}
|
|
962
|
+
catch (err) {
|
|
963
|
+
// A create failure (e.g. HTTP 500 from CF's shared-class rate limit) is
|
|
964
|
+
// a recorded datapoint, not a reason to abort the whole batch.
|
|
965
|
+
fired.push({
|
|
966
|
+
index: i,
|
|
967
|
+
createFailed: true,
|
|
968
|
+
createError: err?.message ? String(err.message) : String(err),
|
|
969
|
+
started: false,
|
|
970
|
+
});
|
|
971
|
+
}
|
|
972
|
+
if (i < count - 1)
|
|
973
|
+
await sleep(intervalMs);
|
|
974
|
+
}
|
|
975
|
+
// Poll the run list until every non-failed run has executionStartedAt set
|
|
976
|
+
// (queueDelayMs present) or we hit the timeout.
|
|
977
|
+
const byInstance = new Map();
|
|
978
|
+
for (const f of fired) {
|
|
979
|
+
if (f.instanceId)
|
|
980
|
+
byInstance.set(f.instanceId, f);
|
|
981
|
+
}
|
|
982
|
+
const deadline = Date.now() + timeoutSec * 1000;
|
|
983
|
+
const pending = () => fired.filter((f) => !f.createFailed && !f.started).length;
|
|
984
|
+
// The admin run-list endpoint caps each page at 200, so a single large
|
|
985
|
+
// request cannot cover a batch of more than 200 runs — the earliest ones
|
|
986
|
+
// would never be matched and would be falsely reported as not started.
|
|
987
|
+
// Page through with the cursor (runs come back newest-first, so our batch
|
|
988
|
+
// is at the front) until every pending run is matched or we run out of
|
|
989
|
+
// pages. Bound the scan so we never walk the whole run history.
|
|
990
|
+
const PAGE_LIMIT = 200;
|
|
991
|
+
const scanBudget = count + PAGE_LIMIT;
|
|
992
|
+
while (pending() > 0 && Date.now() < deadline) {
|
|
993
|
+
await sleep(1000);
|
|
994
|
+
try {
|
|
995
|
+
let cursor;
|
|
996
|
+
let scanned = 0;
|
|
997
|
+
do {
|
|
998
|
+
const { items, cursor: nextCursor } = await client.listWorkflowRuns(resolvedAppId, workflowId, { limit: PAGE_LIMIT, cursor: cursor ?? undefined });
|
|
999
|
+
for (const run of items || []) {
|
|
1000
|
+
const f = run?.instanceId ? byInstance.get(run.instanceId) : undefined;
|
|
1001
|
+
if (f && !f.started && run.executionStartedAt) {
|
|
1002
|
+
f.started = true;
|
|
1003
|
+
f.queueDelayMs =
|
|
1004
|
+
typeof run.queueDelayMs === "number" ? run.queueDelayMs : null;
|
|
1005
|
+
f.createCallDurationMs =
|
|
1006
|
+
typeof run.createCallDurationMs === "number"
|
|
1007
|
+
? run.createCallDurationMs
|
|
1008
|
+
: null;
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
scanned += items?.length || 0;
|
|
1012
|
+
cursor = nextCursor;
|
|
1013
|
+
} while (pending() > 0 && cursor && scanned < scanBudget);
|
|
1014
|
+
}
|
|
1015
|
+
catch {
|
|
1016
|
+
// Transient list error — keep polling until the deadline.
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
const createFailures = fired.filter((f) => f.createFailed).length;
|
|
1020
|
+
const startedRuns = fired.filter((f) => f.started);
|
|
1021
|
+
const queueDelays = startedRuns.map((f) => f.queueDelayMs);
|
|
1022
|
+
const createDurations = startedRuns.map((f) => f.createCallDurationMs);
|
|
1023
|
+
const queueDist = summarizeLatencyDistribution(queueDelays);
|
|
1024
|
+
const createDist = summarizeLatencyDistribution(createDurations);
|
|
1025
|
+
const notStarted = fired.filter((f) => !f.createFailed && !f.started).length;
|
|
1026
|
+
if (options.json) {
|
|
1027
|
+
json({
|
|
1028
|
+
workflowId,
|
|
1029
|
+
requested: count,
|
|
1030
|
+
created: count - createFailures,
|
|
1031
|
+
createFailures,
|
|
1032
|
+
started: startedRuns.length,
|
|
1033
|
+
notStartedWithinTimeout: notStarted,
|
|
1034
|
+
queueDelayMs: queueDist,
|
|
1035
|
+
createCallDurationMs: createDist,
|
|
1036
|
+
runs: options.verbose
|
|
1037
|
+
? fired.map((f) => ({
|
|
1038
|
+
index: f.index,
|
|
1039
|
+
instanceId: f.instanceId ?? null,
|
|
1040
|
+
createFailed: f.createFailed,
|
|
1041
|
+
createError: f.createError ?? null,
|
|
1042
|
+
started: f.started,
|
|
1043
|
+
queueDelayMs: f.queueDelayMs ?? null,
|
|
1044
|
+
createCallDurationMs: f.createCallDurationMs ?? null,
|
|
1045
|
+
}))
|
|
1046
|
+
: undefined,
|
|
1047
|
+
});
|
|
1048
|
+
return;
|
|
1049
|
+
}
|
|
1050
|
+
divider();
|
|
1051
|
+
keyValue("Requested", count);
|
|
1052
|
+
keyValue("Created", count - createFailures);
|
|
1053
|
+
keyValue("Create failures", createFailures);
|
|
1054
|
+
keyValue("Started", startedRuns.length);
|
|
1055
|
+
keyValue("Not started (timeout)", notStarted);
|
|
1056
|
+
divider();
|
|
1057
|
+
if (queueDist) {
|
|
1058
|
+
info("Queue delay (executionStartedAt - requested):");
|
|
1059
|
+
keyValue(" min", formatDuration(queueDist.min));
|
|
1060
|
+
keyValue(" p50", formatDuration(queueDist.p50));
|
|
1061
|
+
keyValue(" p95", formatDuration(queueDist.p95));
|
|
1062
|
+
keyValue(" max", formatDuration(queueDist.max));
|
|
1063
|
+
}
|
|
1064
|
+
else {
|
|
1065
|
+
warn("No runs started — no queue-delay samples collected.");
|
|
1066
|
+
}
|
|
1067
|
+
if (createDist) {
|
|
1068
|
+
divider();
|
|
1069
|
+
info("Create-call duration:");
|
|
1070
|
+
keyValue(" min", formatDuration(createDist.min));
|
|
1071
|
+
keyValue(" p50", formatDuration(createDist.p50));
|
|
1072
|
+
keyValue(" p95", formatDuration(createDist.p95));
|
|
1073
|
+
keyValue(" max", formatDuration(createDist.max));
|
|
1074
|
+
}
|
|
1075
|
+
if (options.verbose) {
|
|
1076
|
+
divider();
|
|
1077
|
+
console.log(formatTable(fired.map((f) => ({
|
|
1078
|
+
run: f.index,
|
|
1079
|
+
instanceId: f.instanceId ?? "-",
|
|
1080
|
+
status: f.createFailed ? "create-failed" : f.started ? "started" : "pending",
|
|
1081
|
+
queueDelayMs: f.queueDelayMs,
|
|
1082
|
+
createCallDurationMs: f.createCallDurationMs,
|
|
1083
|
+
})), [
|
|
1084
|
+
{ header: "RUN", key: "run" },
|
|
1085
|
+
{ header: "INSTANCE", key: "instanceId", format: formatId },
|
|
1086
|
+
{ header: "STATE", key: "status" },
|
|
1087
|
+
{ header: "DELAY", key: "queueDelayMs", format: formatDuration },
|
|
1088
|
+
{ header: "CREATE", key: "createCallDurationMs", format: formatDuration },
|
|
1089
|
+
]));
|
|
1090
|
+
}
|
|
1091
|
+
});
|
|
840
1092
|
// Runs subcommand
|
|
841
1093
|
const runs = workflows.command("runs").description("Manage workflow runs");
|
|
842
1094
|
// List runs
|
|
@@ -868,6 +1120,8 @@ Examples:
|
|
|
868
1120
|
{ header: "RUN ID", key: "runId", format: formatId },
|
|
869
1121
|
{ header: "STATUS", key: "status", format: formatStatus },
|
|
870
1122
|
{ header: "STARTED", key: "startedAt", format: formatDate },
|
|
1123
|
+
// #1367 — queue delay (requested → execution start).
|
|
1124
|
+
{ header: "DELAY", key: "queueDelayMs", format: formatDuration },
|
|
871
1125
|
{ header: "ENDED", key: "endedAt", format: formatDate },
|
|
872
1126
|
{ header: "PREVIEW", key: "isPreview", format: (v) => v ? "yes" : "" },
|
|
873
1127
|
]));
|
|
@@ -897,7 +1151,13 @@ Examples:
|
|
|
897
1151
|
const run = result.run;
|
|
898
1152
|
keyValue("Run ID", run.runId);
|
|
899
1153
|
keyValue("Status", formatStatus(run.status));
|
|
900
|
-
|
|
1154
|
+
// #1367 — `startedAt` is the request time; `executionStartedAt` is when
|
|
1155
|
+
// Cloudflare actually started running the instance. Queue delay is the
|
|
1156
|
+
// gap. A still-queued run shows "-" for execution start / queue delay.
|
|
1157
|
+
keyValue("Requested", formatDate(run.startedAt));
|
|
1158
|
+
keyValue("Execution started", formatDate(run.executionStartedAt));
|
|
1159
|
+
keyValue("Queue delay", formatDuration(run.queueDelayMs));
|
|
1160
|
+
keyValue("Create call", formatDuration(run.createCallDurationMs));
|
|
901
1161
|
keyValue("Ended", formatDate(run.endedAt));
|
|
902
1162
|
keyValue("Preview", run.isPreview ? "yes" : "no");
|
|
903
1163
|
if (result.instanceStatus) {
|
|
@@ -947,7 +1207,12 @@ Examples:
|
|
|
947
1207
|
info("No step runs found.");
|
|
948
1208
|
return;
|
|
949
1209
|
}
|
|
950
|
-
|
|
1210
|
+
// #1367 — inter-step gap: idle time between one step ending and the
|
|
1211
|
+
// next starting. Only surfaced above a 1s threshold so normal
|
|
1212
|
+
// back-to-back steps stay quiet; a large gap flags a mid-run stall.
|
|
1213
|
+
const STEP_GAP_THRESHOLD_MS = 1000;
|
|
1214
|
+
const rows = annotateStepGaps(items);
|
|
1215
|
+
console.log(formatTable(rows, [
|
|
951
1216
|
{ header: "STEP", key: "stepId" },
|
|
952
1217
|
{ header: "KIND", key: "stepKind" },
|
|
953
1218
|
{
|
|
@@ -958,6 +1223,13 @@ Examples:
|
|
|
958
1223
|
v === "skipped" ? chalk.gray(v) :
|
|
959
1224
|
v === "error_captured" ? chalk.yellow(v) : v,
|
|
960
1225
|
},
|
|
1226
|
+
{
|
|
1227
|
+
header: "GAP",
|
|
1228
|
+
key: "gapMs",
|
|
1229
|
+
format: (v) => typeof v === "number" && v >= STEP_GAP_THRESHOLD_MS
|
|
1230
|
+
? chalk.yellow(formatDuration(v))
|
|
1231
|
+
: chalk.dim("-"),
|
|
1232
|
+
},
|
|
961
1233
|
{ header: "DURATION", key: "durationMs", format: formatDuration },
|
|
962
1234
|
{
|
|
963
1235
|
header: "TOKENS",
|
|
@@ -2472,91 +2744,31 @@ Examples:
|
|
|
2472
2744
|
.option("--json", "Output the result summary as JSON")
|
|
2473
2745
|
.action(async (workflowKey, options) => {
|
|
2474
2746
|
try {
|
|
2475
|
-
// 1.
|
|
2476
|
-
//
|
|
2477
|
-
//
|
|
2478
|
-
//
|
|
2479
|
-
//
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
const candidate = path.join(options.syncDir, "workflows");
|
|
2485
|
-
if (existsSync(candidate)) {
|
|
2486
|
-
workflowDirs.push(candidate);
|
|
2487
|
-
}
|
|
2488
|
-
}
|
|
2489
|
-
else {
|
|
2490
|
-
const root = path.join(process.cwd(), ".primitive", "sync");
|
|
2491
|
-
if (existsSync(root)) {
|
|
2492
|
-
for (const env of readdirSync(root)) {
|
|
2493
|
-
const envDir = path.join(root, env);
|
|
2494
|
-
try {
|
|
2495
|
-
for (const app of readdirSync(envDir)) {
|
|
2496
|
-
if (options.app && app !== options.app)
|
|
2497
|
-
continue;
|
|
2498
|
-
const candidate = path.join(envDir, app, "workflows");
|
|
2499
|
-
if (existsSync(candidate)) {
|
|
2500
|
-
workflowDirs.push(candidate);
|
|
2501
|
-
}
|
|
2502
|
-
}
|
|
2503
|
-
}
|
|
2504
|
-
catch {
|
|
2505
|
-
// skip non-dirs
|
|
2506
|
-
}
|
|
2507
|
-
}
|
|
2508
|
-
}
|
|
2509
|
-
}
|
|
2510
|
-
if (workflowDirs.length === 0) {
|
|
2511
|
-
error(options.app
|
|
2512
|
-
? `No workflows/ directory found for app "${options.app}" under .primitive/sync/. Check the --app id, pass --sync-dir, or run \`primitive sync pull\` first.`
|
|
2513
|
-
: "No workflows/ directory found. Pass --sync-dir or run `primitive sync pull` first.");
|
|
2514
|
-
process.exit(1);
|
|
2515
|
-
}
|
|
2516
|
-
// Codegen writes one <key>.generated.ts per workflow into a single
|
|
2517
|
-
// output dir derived from the first source dir below. Generating from
|
|
2518
|
-
// more than one discovered workflows/ directory (multiple apps, or the
|
|
2519
|
-
// same app across multiple synced envs) would combine unrelated apps'
|
|
2520
|
-
// workflows into the first one's output dir and write nothing for the
|
|
2521
|
-
// rest. Require the caller to disambiguate rather than combining.
|
|
2522
|
-
if (workflowDirs.length > 1) {
|
|
2523
|
-
const appsFound = [
|
|
2524
|
-
...new Set(workflowDirs.map((d) => path.basename(path.dirname(d)))),
|
|
2525
|
-
];
|
|
2526
|
-
error(`Found workflows/ directories for more than one app/env:\n` +
|
|
2527
|
-
workflowDirs
|
|
2528
|
-
.map((d) => ` ${path.relative(process.cwd(), d)}`)
|
|
2529
|
-
.join("\n") +
|
|
2530
|
-
`\nCodegen writes into one output dir, so it must run against` +
|
|
2531
|
-
` one app. ` +
|
|
2532
|
-
(options.app
|
|
2533
|
-
? `App "${options.app}" resolves to multiple synced envs — pass --sync-dir to pick one.`
|
|
2534
|
-
: `Pass --app <app-id> (found: ${appsFound.join(", ")}) or` +
|
|
2535
|
-
` --sync-dir to select one.`));
|
|
2536
|
-
process.exit(1);
|
|
2537
|
-
}
|
|
2747
|
+
// 1. Resolve the single source workflows/ directory via the shared
|
|
2748
|
+
// active-environment resolver (issue #1510). Honors --sync-dir /
|
|
2749
|
+
// --app overrides, resolves the active env
|
|
2750
|
+
// (--env → PRIMITIVE_ENV → defaultEnvironment → single-env), and
|
|
2751
|
+
// preserves the legacy per-app scan fallback in bare-dir mode.
|
|
2752
|
+
const workflowsSourceDir = resolveCodegenSourceDir({
|
|
2753
|
+
subdir: "workflows",
|
|
2754
|
+
options: { app: options.app, syncDir: options.syncDir },
|
|
2755
|
+
});
|
|
2538
2756
|
// 2. Collect the source .toml files (one per workflow), filtered to a
|
|
2539
2757
|
// single workflow when an argument is given. Match on the file stem;
|
|
2540
2758
|
// the generator resolves the real key from `[workflow].key`.
|
|
2541
2759
|
const inputs = [];
|
|
2542
|
-
const
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
inputs.push({
|
|
2555
|
-
fileStem: stem,
|
|
2556
|
-
tomlPath,
|
|
2557
|
-
tomlContent: readFileSync(tomlPath, "utf-8"),
|
|
2558
|
-
});
|
|
2559
|
-
}
|
|
2760
|
+
for (const fileName of readdirSync(workflowsSourceDir)) {
|
|
2761
|
+
if (!fileName.endsWith(".toml"))
|
|
2762
|
+
continue;
|
|
2763
|
+
const stem = fileName.slice(0, -".toml".length);
|
|
2764
|
+
if (workflowKey && stem !== workflowKey)
|
|
2765
|
+
continue;
|
|
2766
|
+
const tomlPath = path.join(workflowsSourceDir, fileName);
|
|
2767
|
+
inputs.push({
|
|
2768
|
+
fileStem: stem,
|
|
2769
|
+
tomlPath,
|
|
2770
|
+
tomlContent: readFileSync(tomlPath, "utf-8"),
|
|
2771
|
+
});
|
|
2560
2772
|
}
|
|
2561
2773
|
if (inputs.length === 0) {
|
|
2562
2774
|
error(workflowKey
|