@workflow-code/cli 0.1.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/.env.example +5 -0
- package/README.md +8 -0
- package/dist/index.js +51 -0
- package/dist/local-XBULJ25O.js +337 -0
- package/dist/workspace-WJOCEGA6.js +1044 -0
- package/package.json +37 -0
|
@@ -0,0 +1,1044 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/workspace.ts
|
|
4
|
+
import { spawn } from "child_process";
|
|
5
|
+
import { randomUUID } from "crypto";
|
|
6
|
+
import { existsSync as existsSync2 } from "fs";
|
|
7
|
+
import { cp, mkdir as mkdir2, readFile as readFile2, rm as rm2, writeFile as writeFile2 } from "fs/promises";
|
|
8
|
+
import path3 from "path";
|
|
9
|
+
import { fileURLToPath, pathToFileURL } from "url";
|
|
10
|
+
|
|
11
|
+
// ../../shared/auth-config/index.ts
|
|
12
|
+
var WORKFLOW_AUTH_FILE_NAME = "workflow-auth.json";
|
|
13
|
+
var DEFAULT_MAC_PLATFORM = "darwin";
|
|
14
|
+
var DEFAULT_WINDOWS_PLATFORM = "win32";
|
|
15
|
+
var DEFAULT_OTHER_PLATFORM = "linux";
|
|
16
|
+
function createEmptyWorkflowAuthConfig() {
|
|
17
|
+
return {
|
|
18
|
+
serverUrl: "",
|
|
19
|
+
apiKey: ""
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
function normalizeWorkflowAuthConfig(value) {
|
|
23
|
+
if (!isRecord(value)) {
|
|
24
|
+
return createEmptyWorkflowAuthConfig();
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
// issue #90 Accept legacy field names (remoteUrl, token) from pre-migration
|
|
28
|
+
// config files so existing users don't lose their credentials on upgrade.
|
|
29
|
+
serverUrl: readString(value.serverUrl) ?? readString(value.remoteUrl) ?? "",
|
|
30
|
+
apiKey: readString(value.apiKey) ?? readString(value.token) ?? "",
|
|
31
|
+
expiresAt: readString(value.expiresAt),
|
|
32
|
+
user: isRecord(value.user) ? {
|
|
33
|
+
userId: readString(value.user.userId) ?? "",
|
|
34
|
+
email: readString(value.user.email) ?? "",
|
|
35
|
+
displayName: readString(value.user.displayName) ?? "",
|
|
36
|
+
avatarUrl: readString(value.user.avatarUrl)
|
|
37
|
+
} : void 0,
|
|
38
|
+
source: value.source === "device_flow" || value.source === "manual" || value.source === "password" ? value.source : void 0
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function isWorkflowAuthExpiringSoon(expiresAt, options = {}) {
|
|
42
|
+
if (!expiresAt) {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
const expiresAtMs = new Date(expiresAt).getTime();
|
|
46
|
+
if (!Number.isFinite(expiresAtMs)) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
const now = options.now ?? Date.now();
|
|
50
|
+
const thresholdMs = options.thresholdMs ?? 1e3 * 60 * 60 * 24 * 3;
|
|
51
|
+
return expiresAtMs - now <= thresholdMs;
|
|
52
|
+
}
|
|
53
|
+
function resolveWorkflowCliAuthDir(env = readProcessEnv()) {
|
|
54
|
+
if (readString(env.WORKFLOW_CLI_AUTH_DIR)) {
|
|
55
|
+
return normalizePath(env.WORKFLOW_CLI_AUTH_DIR);
|
|
56
|
+
}
|
|
57
|
+
if (readPlatform(env) === DEFAULT_WINDOWS_PLATFORM) {
|
|
58
|
+
return joinPath(
|
|
59
|
+
env.APPDATA ?? joinPath(resolveHomeDir(env), "AppData", "Roaming"),
|
|
60
|
+
"workflow-code"
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
if (readPlatform(env) === DEFAULT_MAC_PLATFORM) {
|
|
64
|
+
return joinPath(resolveHomeDir(env), "Library", "Application Support", "workflow-code");
|
|
65
|
+
}
|
|
66
|
+
return joinPath(
|
|
67
|
+
env.XDG_CONFIG_HOME ?? joinPath(resolveHomeDir(env), ".config"),
|
|
68
|
+
"workflow-code"
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
function resolveWorkflowCliAuthFilePath(env = readProcessEnv()) {
|
|
72
|
+
return joinPath(resolveWorkflowCliAuthDir(env), WORKFLOW_AUTH_FILE_NAME);
|
|
73
|
+
}
|
|
74
|
+
function resolveHomeDir(env) {
|
|
75
|
+
return env.HOME ?? "";
|
|
76
|
+
}
|
|
77
|
+
function readPlatform(env) {
|
|
78
|
+
return env.platform ?? readProcessPlatform();
|
|
79
|
+
}
|
|
80
|
+
function isRecord(value) {
|
|
81
|
+
return typeof value === "object" && value !== null;
|
|
82
|
+
}
|
|
83
|
+
function readString(value) {
|
|
84
|
+
return typeof value === "string" && value.trim() !== "" ? value.trim() : void 0;
|
|
85
|
+
}
|
|
86
|
+
function joinPath(...parts) {
|
|
87
|
+
return normalizePath(parts.filter((part) => part !== "").join("/"));
|
|
88
|
+
}
|
|
89
|
+
function normalizePath(value) {
|
|
90
|
+
const normalized = value.replaceAll("\\", "/");
|
|
91
|
+
if (/^[A-Za-z]:\//.test(normalized)) {
|
|
92
|
+
const drive = normalized.slice(0, 2);
|
|
93
|
+
const rest = normalized.slice(2).replace(/\/+/g, "/");
|
|
94
|
+
return `${drive}${rest.startsWith("/") ? rest : `/${rest}`}`;
|
|
95
|
+
}
|
|
96
|
+
return normalized.replace(/\/+/g, "/");
|
|
97
|
+
}
|
|
98
|
+
function readProcessEnv() {
|
|
99
|
+
const processValue = readGlobalProcess();
|
|
100
|
+
return {
|
|
101
|
+
...processValue?.env ?? {},
|
|
102
|
+
platform: processValue?.platform ?? DEFAULT_OTHER_PLATFORM
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
function readProcessPlatform() {
|
|
106
|
+
return readGlobalProcess()?.platform ?? DEFAULT_OTHER_PLATFORM;
|
|
107
|
+
}
|
|
108
|
+
function readGlobalProcess() {
|
|
109
|
+
const processValue = Reflect.get(globalThis, "process");
|
|
110
|
+
if (typeof processValue !== "object" || processValue === null) {
|
|
111
|
+
return void 0;
|
|
112
|
+
}
|
|
113
|
+
return processValue;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ../../shared/workflow-ids/index.ts
|
|
117
|
+
import path from "path";
|
|
118
|
+
var WORKFLOW_UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
119
|
+
function isWorkflowUuid(value) {
|
|
120
|
+
return WORKFLOW_UUID_PATTERN.test(value.trim());
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// src/auth-config.ts
|
|
124
|
+
import { readFileSync } from "fs";
|
|
125
|
+
import { mkdir, readFile, rm, writeFile } from "fs/promises";
|
|
126
|
+
async function readCliAuthConfig(env = process.env) {
|
|
127
|
+
try {
|
|
128
|
+
const raw = await readFile(resolveWorkflowCliAuthFilePath(env), "utf8");
|
|
129
|
+
return normalizeWorkflowAuthConfig(JSON.parse(raw));
|
|
130
|
+
} catch {
|
|
131
|
+
return createEmptyWorkflowAuthConfig();
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
function readCliAuthConfigSync(env = process.env) {
|
|
135
|
+
try {
|
|
136
|
+
const raw = readFileSync(resolveWorkflowCliAuthFilePath(env), "utf8");
|
|
137
|
+
return normalizeWorkflowAuthConfig(JSON.parse(raw));
|
|
138
|
+
} catch {
|
|
139
|
+
return createEmptyWorkflowAuthConfig();
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
async function writeCliAuthConfig(config, env = process.env) {
|
|
143
|
+
await mkdir(resolveWorkflowCliAuthDir(env), { recursive: true });
|
|
144
|
+
await writeFile(
|
|
145
|
+
resolveWorkflowCliAuthFilePath(env),
|
|
146
|
+
`${JSON.stringify(normalizeWorkflowAuthConfig(config), null, 2)}
|
|
147
|
+
`,
|
|
148
|
+
"utf8"
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
async function clearCliAuthConfig(env = process.env) {
|
|
152
|
+
await rm(resolveWorkflowCliAuthFilePath(env), { force: true });
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// src/env.ts
|
|
156
|
+
import { existsSync, readFileSync as readFileSync2 } from "fs";
|
|
157
|
+
import path2 from "path";
|
|
158
|
+
import { parse } from "dotenv";
|
|
159
|
+
function loadCliEnv() {
|
|
160
|
+
const cliRoot = process.cwd();
|
|
161
|
+
const defaultRepoRoot = findRepoRoot(cliRoot) ?? cliRoot;
|
|
162
|
+
const initialRepoRoot = process.env.WORKFLOW_REPO_ROOT ? path2.resolve(process.env.WORKFLOW_REPO_ROOT) : defaultRepoRoot;
|
|
163
|
+
loadEnvFiles([
|
|
164
|
+
path2.join(cliRoot, ".env"),
|
|
165
|
+
path2.join(initialRepoRoot, ".env")
|
|
166
|
+
]);
|
|
167
|
+
const resolvedRepoRoot = process.env.WORKFLOW_REPO_ROOT ? path2.resolve(process.env.WORKFLOW_REPO_ROOT) : initialRepoRoot;
|
|
168
|
+
if (resolvedRepoRoot !== initialRepoRoot) {
|
|
169
|
+
loadEnvFiles([path2.join(resolvedRepoRoot, ".env")]);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
function findRepoRoot(startDir) {
|
|
173
|
+
let current = path2.resolve(startDir);
|
|
174
|
+
while (true) {
|
|
175
|
+
if (existsSync(path2.join(current, "pnpm-workspace.yaml")) && existsSync(path2.join(current, "packages", "cli", "package.json"))) {
|
|
176
|
+
return current;
|
|
177
|
+
}
|
|
178
|
+
const parent = path2.dirname(current);
|
|
179
|
+
if (parent === current) return void 0;
|
|
180
|
+
current = parent;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
function loadEnvFiles(filePaths) {
|
|
184
|
+
for (const filePath of unique(filePaths)) {
|
|
185
|
+
if (!existsSync(filePath)) {
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
const values = parse(readFileSync2(filePath));
|
|
189
|
+
for (const [key, value] of Object.entries(values)) {
|
|
190
|
+
if (process.env[key] === void 0) {
|
|
191
|
+
process.env[key] = value;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
function unique(values) {
|
|
197
|
+
return [...new Set(values)];
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// src/workspace.ts
|
|
201
|
+
var DEFAULT_SERVER_URL = "http://localhost:7125";
|
|
202
|
+
var CLI_MODULE_DIR = path3.dirname(fileURLToPath(import.meta.url));
|
|
203
|
+
var DEFAULT_DEVICE_FLOW_CLIENT_NAME = "Workflow Workspace CLI";
|
|
204
|
+
loadCliEnv();
|
|
205
|
+
async function main(argv = process.argv.slice(2)) {
|
|
206
|
+
const parsed = parseArgs(argv);
|
|
207
|
+
switch (parsed.command) {
|
|
208
|
+
case "login":
|
|
209
|
+
await commandLogin(parsed);
|
|
210
|
+
return;
|
|
211
|
+
case "logout":
|
|
212
|
+
await commandLogout();
|
|
213
|
+
return;
|
|
214
|
+
case "status":
|
|
215
|
+
await commandStatus();
|
|
216
|
+
return;
|
|
217
|
+
case "pack":
|
|
218
|
+
await commandPack(parsed);
|
|
219
|
+
return;
|
|
220
|
+
case "upload":
|
|
221
|
+
await commandUpload(parsed);
|
|
222
|
+
return;
|
|
223
|
+
case "preparation":
|
|
224
|
+
await commandPreparation(parsed);
|
|
225
|
+
return;
|
|
226
|
+
case "run":
|
|
227
|
+
await commandRun(parsed);
|
|
228
|
+
return;
|
|
229
|
+
case "debug-node":
|
|
230
|
+
await commandDebugNode(parsed);
|
|
231
|
+
return;
|
|
232
|
+
case "download":
|
|
233
|
+
await commandDownload(parsed);
|
|
234
|
+
return;
|
|
235
|
+
case "publish":
|
|
236
|
+
await commandPublish(parsed);
|
|
237
|
+
return;
|
|
238
|
+
case "versions":
|
|
239
|
+
await commandVersions(parsed);
|
|
240
|
+
return;
|
|
241
|
+
case "health":
|
|
242
|
+
await commandHealth(parsed);
|
|
243
|
+
return;
|
|
244
|
+
case "":
|
|
245
|
+
case "help":
|
|
246
|
+
case "--help":
|
|
247
|
+
case "-h":
|
|
248
|
+
printUsage();
|
|
249
|
+
return;
|
|
250
|
+
default:
|
|
251
|
+
throw new Error(`Unknown command: ${parsed.command}`);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
async function commandLogin(parsed) {
|
|
255
|
+
const server = parsed.options.server.trim();
|
|
256
|
+
if (server === "") {
|
|
257
|
+
throw new Error("login requires --server.");
|
|
258
|
+
}
|
|
259
|
+
const started = await fetchApi({
|
|
260
|
+
server,
|
|
261
|
+
token: "",
|
|
262
|
+
path: "/api/auth/device/start",
|
|
263
|
+
method: "POST",
|
|
264
|
+
body: {
|
|
265
|
+
// issue #93 phase 1 moves workspace login to device flow and stores a
|
|
266
|
+
// dedicated CLI auth file instead of sharing the App config.
|
|
267
|
+
clientName: DEFAULT_DEVICE_FLOW_CLIENT_NAME,
|
|
268
|
+
source: "cli"
|
|
269
|
+
},
|
|
270
|
+
auth: false
|
|
271
|
+
});
|
|
272
|
+
if (started.errCode !== 0 || started.data === void 0) {
|
|
273
|
+
printApiResponse(started);
|
|
274
|
+
assertApiOk(started);
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
const verificationUrl = started.data.verificationUriComplete || started.data.verificationUri;
|
|
278
|
+
void openExternalBrowser(verificationUrl);
|
|
279
|
+
console.error(`Open the verification page to approve this login:
|
|
280
|
+
${verificationUrl}
|
|
281
|
+
User code: ${started.data.userCode}`);
|
|
282
|
+
const completed = await waitForDeviceToken({
|
|
283
|
+
server,
|
|
284
|
+
deviceCode: started.data.deviceCode,
|
|
285
|
+
intervalSeconds: started.data.intervalSeconds,
|
|
286
|
+
expiresAt: started.data.expiresAt
|
|
287
|
+
});
|
|
288
|
+
if (completed.errCode !== 0 || completed.data === void 0) {
|
|
289
|
+
printApiResponse(completed);
|
|
290
|
+
assertApiOk(completed);
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
await writeCliAuthConfig({
|
|
294
|
+
serverUrl: server,
|
|
295
|
+
apiKey: completed.data.accessToken,
|
|
296
|
+
expiresAt: completed.data.expiresAt ?? completed.data.apiKey.expiresAt,
|
|
297
|
+
user: completed.data.user,
|
|
298
|
+
source: "device_flow"
|
|
299
|
+
});
|
|
300
|
+
printApiResponse({
|
|
301
|
+
errCode: 0,
|
|
302
|
+
errMessage: "",
|
|
303
|
+
data: {
|
|
304
|
+
loggedIn: true,
|
|
305
|
+
serverUrl: server,
|
|
306
|
+
user: completed.data.user,
|
|
307
|
+
expiresAt: completed.data.expiresAt ?? completed.data.apiKey.expiresAt,
|
|
308
|
+
apiKeyPrefix: completed.data.apiKey.prefix
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
async function commandLogout() {
|
|
313
|
+
await clearCliAuthConfig();
|
|
314
|
+
printApiResponse({ errCode: 0, errMessage: "", data: { loggedIn: false } });
|
|
315
|
+
}
|
|
316
|
+
async function commandStatus() {
|
|
317
|
+
const authConfig = await readCliAuthConfig();
|
|
318
|
+
printApiResponse({
|
|
319
|
+
errCode: 0,
|
|
320
|
+
errMessage: "",
|
|
321
|
+
data: {
|
|
322
|
+
loggedIn: authConfig.serverUrl.trim() !== "" && authConfig.apiKey.trim() !== "",
|
|
323
|
+
serverUrl: authConfig.serverUrl,
|
|
324
|
+
user: authConfig.user,
|
|
325
|
+
expiresAt: authConfig.expiresAt,
|
|
326
|
+
expiringSoon: isWorkflowAuthExpiringSoon(authConfig.expiresAt),
|
|
327
|
+
source: authConfig.source ?? null
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
async function commandPack(parsed) {
|
|
332
|
+
const workflowName = requirePositional(parsed, 0, "workflow");
|
|
333
|
+
const archivePath = await packWorkflow(workflowName, parsed.options.output, parsed.options.path);
|
|
334
|
+
console.log(JSON.stringify({ archivePath }, null, 2));
|
|
335
|
+
}
|
|
336
|
+
async function commandUpload(parsed) {
|
|
337
|
+
requireLogin(parsed);
|
|
338
|
+
const workflowArg = requirePositional(parsed, 0, "workflow");
|
|
339
|
+
const packageContext = await readWorkflowPackageContext(workflowArg, parsed.options.path);
|
|
340
|
+
let workflowId = packageContext.workflowId;
|
|
341
|
+
if (parsed.options.create === true) {
|
|
342
|
+
const created = await ensureWorkflowProjectCreated({
|
|
343
|
+
packageContext,
|
|
344
|
+
server: parsed.options.server,
|
|
345
|
+
token: parsed.options.token
|
|
346
|
+
});
|
|
347
|
+
workflowId = created.workflowId;
|
|
348
|
+
}
|
|
349
|
+
if (!workflowId) {
|
|
350
|
+
throw new Error("Workflow package.json must include a UUID id. Use `workflow-code workspace upload <workflow> --create` after login to create and bind a server project.");
|
|
351
|
+
}
|
|
352
|
+
const archivePath = parsed.options.file ? path3.resolve(parsed.options.file) : await packWorkflow(workflowArg, parsed.options.output, parsed.options.path);
|
|
353
|
+
const uploadResult = await uploadPackage({
|
|
354
|
+
workflowId,
|
|
355
|
+
archivePath,
|
|
356
|
+
server: parsed.options.server,
|
|
357
|
+
token: parsed.options.token
|
|
358
|
+
});
|
|
359
|
+
if (uploadResult.errCode !== 0 || uploadResult.data === void 0) {
|
|
360
|
+
printApiResponse(uploadResult);
|
|
361
|
+
assertApiOk(uploadResult);
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
if (parsed.options.noWait === true) {
|
|
365
|
+
printApiResponse(uploadResult);
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
const preparationResult = await waitForPreparation({
|
|
369
|
+
workflowId,
|
|
370
|
+
jobId: uploadResult.data.jobId,
|
|
371
|
+
server: parsed.options.server,
|
|
372
|
+
token: parsed.options.token
|
|
373
|
+
});
|
|
374
|
+
printApiResponse(preparationResult);
|
|
375
|
+
assertApiOk(preparationResult);
|
|
376
|
+
if (preparationResult.errCode !== 0) {
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
if (parsed.options.releaseLog !== void 0) {
|
|
380
|
+
const publishResult = await fetchApi({
|
|
381
|
+
server: parsed.options.server,
|
|
382
|
+
token: parsed.options.token,
|
|
383
|
+
path: `/api/workflows/${encodeURIComponent(workflowId)}/publish`,
|
|
384
|
+
method: "POST",
|
|
385
|
+
body: {
|
|
386
|
+
releaseLog: parsed.options.releaseLog,
|
|
387
|
+
sourceMode: parsed.options.sourceMode ?? "bundled"
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
printApiResponse(publishResult);
|
|
391
|
+
assertApiOk(publishResult);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
async function commandPreparation(parsed) {
|
|
395
|
+
requireLogin(parsed);
|
|
396
|
+
const workflowId = requirePositional(parsed, 0, "workflow");
|
|
397
|
+
const jobId = requirePositional(parsed, 1, "jobId");
|
|
398
|
+
const result = await getPreparation({
|
|
399
|
+
workflowId,
|
|
400
|
+
jobId,
|
|
401
|
+
server: parsed.options.server,
|
|
402
|
+
token: parsed.options.token
|
|
403
|
+
});
|
|
404
|
+
printApiResponse(result);
|
|
405
|
+
assertApiOk(result);
|
|
406
|
+
}
|
|
407
|
+
async function commandRun(parsed) {
|
|
408
|
+
requireLogin(parsed);
|
|
409
|
+
const workflowId = await resolveRemoteWorkflowId(parsed);
|
|
410
|
+
const workflowArgs = parsed.positional.slice(1);
|
|
411
|
+
const result = await fetchApi({
|
|
412
|
+
server: parsed.options.server,
|
|
413
|
+
token: parsed.options.token,
|
|
414
|
+
path: `/api/workflows/${encodeURIComponent(workflowId)}/run`,
|
|
415
|
+
method: "POST",
|
|
416
|
+
body: {
|
|
417
|
+
target: parsed.options.target ?? "latest",
|
|
418
|
+
args: workflowArgs
|
|
419
|
+
}
|
|
420
|
+
});
|
|
421
|
+
printApiResponse(result);
|
|
422
|
+
assertApiOk(result);
|
|
423
|
+
}
|
|
424
|
+
async function commandDebugNode(parsed) {
|
|
425
|
+
requireLogin(parsed);
|
|
426
|
+
const workflowId = await resolveRemoteWorkflowId(parsed);
|
|
427
|
+
const nodeName = requirePositional(parsed, 1, "node");
|
|
428
|
+
const workflowArgs = parsed.positional.slice(2);
|
|
429
|
+
const result = await fetchApi({
|
|
430
|
+
server: parsed.options.server,
|
|
431
|
+
token: parsed.options.token,
|
|
432
|
+
path: `/api/workflows/${encodeURIComponent(workflowId)}/debug/nodes/${encodeURIComponent(nodeName)}`,
|
|
433
|
+
method: "POST",
|
|
434
|
+
body: {
|
|
435
|
+
target: parsed.options.target ?? "latest",
|
|
436
|
+
args: workflowArgs
|
|
437
|
+
}
|
|
438
|
+
});
|
|
439
|
+
printApiResponse(result);
|
|
440
|
+
assertApiOk(result);
|
|
441
|
+
}
|
|
442
|
+
async function commandVersions(parsed) {
|
|
443
|
+
requireLogin(parsed);
|
|
444
|
+
const workflowId = await resolveRemoteWorkflowId(parsed);
|
|
445
|
+
const result = await fetchApi({
|
|
446
|
+
server: parsed.options.server,
|
|
447
|
+
token: parsed.options.token,
|
|
448
|
+
path: `/api/workflows/${encodeURIComponent(workflowId)}/versions`
|
|
449
|
+
});
|
|
450
|
+
printApiResponse(result);
|
|
451
|
+
assertApiOk(result);
|
|
452
|
+
}
|
|
453
|
+
async function commandDownload(parsed) {
|
|
454
|
+
requireLogin(parsed);
|
|
455
|
+
const workflowArg = requirePositional(parsed, 0, "workflow");
|
|
456
|
+
const workflowId = await resolveRemoteWorkflowId(parsed);
|
|
457
|
+
const target = parsed.options.target ?? "draft";
|
|
458
|
+
const result = await fetchApi({
|
|
459
|
+
server: parsed.options.server,
|
|
460
|
+
token: parsed.options.token,
|
|
461
|
+
path: `/api/workflows/${encodeURIComponent(workflowId)}/files?target=${encodeURIComponent(target)}`
|
|
462
|
+
});
|
|
463
|
+
printApiResponse(result);
|
|
464
|
+
assertApiOk(result);
|
|
465
|
+
if (result.errCode !== 0 || result.data === void 0) {
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
const repoRoot = resolveRepoRoot();
|
|
469
|
+
const targetDir = parsed.options.path ? path3.resolve(parsed.options.path) : path3.join(repoRoot, "workspace", "workflow", workflowArg);
|
|
470
|
+
await rm2(targetDir, { recursive: true, force: true });
|
|
471
|
+
await mkdir2(targetDir, { recursive: true });
|
|
472
|
+
for (const file of result.data.files) {
|
|
473
|
+
const safePath = normalizeWorkflowFilePath(file.path);
|
|
474
|
+
const targetPath = path3.join(targetDir, safePath);
|
|
475
|
+
await mkdir2(path3.dirname(targetPath), { recursive: true });
|
|
476
|
+
await writeFile2(targetPath, file.content);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
async function commandPublish(parsed) {
|
|
480
|
+
requireLogin(parsed);
|
|
481
|
+
const workflowId = await resolveRemoteWorkflowId(parsed);
|
|
482
|
+
const publishResult = await fetchApi({
|
|
483
|
+
server: parsed.options.server,
|
|
484
|
+
token: parsed.options.token,
|
|
485
|
+
path: `/api/workflows/${encodeURIComponent(workflowId)}/publish`,
|
|
486
|
+
method: "POST",
|
|
487
|
+
body: {
|
|
488
|
+
releaseLog: parsed.options.releaseLog,
|
|
489
|
+
sourceMode: parsed.options.sourceMode ?? "bundled"
|
|
490
|
+
}
|
|
491
|
+
});
|
|
492
|
+
printApiResponse(publishResult);
|
|
493
|
+
assertApiOk(publishResult);
|
|
494
|
+
}
|
|
495
|
+
async function commandHealth(parsed) {
|
|
496
|
+
const result = await fetchApi({
|
|
497
|
+
server: parsed.options.server,
|
|
498
|
+
token: "",
|
|
499
|
+
path: "/health",
|
|
500
|
+
auth: false
|
|
501
|
+
});
|
|
502
|
+
printApiResponse(result);
|
|
503
|
+
assertApiOk(result);
|
|
504
|
+
}
|
|
505
|
+
async function packWorkflow(workflowName, outputPath, sourcePath) {
|
|
506
|
+
const repoRoot = resolveRepoRoot();
|
|
507
|
+
const workflowDir = sourcePath === void 0 ? path3.join(repoRoot, "workspace", "workflow", workflowName) : path3.resolve(sourcePath);
|
|
508
|
+
if (!existsSync2(path3.join(workflowDir, "README.md"))) {
|
|
509
|
+
throw new Error(`Workflow project "${workflowDir}" must contain README.md before upload.`);
|
|
510
|
+
}
|
|
511
|
+
const packsDir = process.env.WORKFLOW_WORKSPACE_PACKS_DIR ? path3.resolve(process.env.WORKFLOW_WORKSPACE_PACKS_DIR) : path3.join(repoRoot, "workspace", ".packs");
|
|
512
|
+
const stagingDir = path3.join(packsDir, "tmp", randomUUID(), workflowName);
|
|
513
|
+
const archivePath = outputPath ? path3.resolve(outputPath) : path3.join(packsDir, `${workflowName}-${createTimestamp()}.tgz`);
|
|
514
|
+
const workspaceRoot = sourcePath === void 0 ? path3.join(repoRoot, "workspace", "workflow") : path3.dirname(workflowDir);
|
|
515
|
+
await rm2(path3.dirname(stagingDir), { recursive: true, force: true });
|
|
516
|
+
await mkdir2(path3.join(stagingDir, "workspace", "workflow"), { recursive: true });
|
|
517
|
+
await mkdir2(path3.dirname(archivePath), { recursive: true });
|
|
518
|
+
await stageWorkflowWithLocalDependencies({
|
|
519
|
+
workspaceRoot,
|
|
520
|
+
workflowDir,
|
|
521
|
+
workflowName,
|
|
522
|
+
stagingRoot: path3.join(stagingDir, "workspace", "workflow")
|
|
523
|
+
});
|
|
524
|
+
const result = await runCommand("tar", ["-czf", archivePath, "-C", stagingDir, "."], {
|
|
525
|
+
cwd: repoRoot
|
|
526
|
+
});
|
|
527
|
+
await rm2(path3.dirname(stagingDir), { recursive: true, force: true });
|
|
528
|
+
if (result.exitCode !== 0) {
|
|
529
|
+
throw new Error(`Failed to create archive: ${result.stderr || result.stdout}`);
|
|
530
|
+
}
|
|
531
|
+
return archivePath;
|
|
532
|
+
}
|
|
533
|
+
async function resolveRemoteWorkflowId(parsed) {
|
|
534
|
+
const workflowArg = requirePositional(parsed, 0, "workflow");
|
|
535
|
+
if (isWorkflowUuid(workflowArg)) {
|
|
536
|
+
return workflowArg;
|
|
537
|
+
}
|
|
538
|
+
const packageContext = await readWorkflowPackageContext(workflowArg, parsed.options.path);
|
|
539
|
+
if (packageContext.workflowId) {
|
|
540
|
+
return packageContext.workflowId;
|
|
541
|
+
}
|
|
542
|
+
throw new Error(`Workflow "${workflowArg}" is not bound to a server UUID. Create or bind it first with \`workflow-code workspace upload ${workflowArg} --create\`.`);
|
|
543
|
+
}
|
|
544
|
+
async function readWorkflowPackageContext(workflowArg, sourcePath) {
|
|
545
|
+
const repoRoot = resolveRepoRoot();
|
|
546
|
+
const workflowDir = sourcePath === void 0 ? path3.join(repoRoot, "workspace", "workflow", workflowArg) : path3.resolve(sourcePath);
|
|
547
|
+
const packageJsonPath = path3.join(workflowDir, "package.json");
|
|
548
|
+
const packageJson = await readPackageJsonFile(packageJsonPath);
|
|
549
|
+
const workflowId = typeof packageJson.id === "string" && isWorkflowUuid(packageJson.id) ? packageJson.id.trim() : void 0;
|
|
550
|
+
const workflowName = readRequiredPackageString(packageJson, "name");
|
|
551
|
+
return {
|
|
552
|
+
workflowArg,
|
|
553
|
+
workflowDir,
|
|
554
|
+
packageJsonPath,
|
|
555
|
+
workflowId,
|
|
556
|
+
workflowName,
|
|
557
|
+
packageJson
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
async function ensureWorkflowProjectCreated(options) {
|
|
561
|
+
const created = await fetchApi({
|
|
562
|
+
server: options.server,
|
|
563
|
+
token: options.token,
|
|
564
|
+
path: "/api/workflows",
|
|
565
|
+
method: "POST",
|
|
566
|
+
body: {
|
|
567
|
+
name: options.packageContext.workflowName,
|
|
568
|
+
...options.packageContext.workflowId ? { workflowId: options.packageContext.workflowId } : {}
|
|
569
|
+
}
|
|
570
|
+
});
|
|
571
|
+
if (created.errCode !== 0 || created.data === void 0) {
|
|
572
|
+
printApiResponse(created);
|
|
573
|
+
assertApiOk(created);
|
|
574
|
+
throw new Error("Workflow project creation failed.");
|
|
575
|
+
}
|
|
576
|
+
if (!options.packageContext.workflowId || options.packageContext.workflowId !== created.data.workflowId) {
|
|
577
|
+
await writeWorkflowPackageId(options.packageContext.packageJsonPath, created.data.workflowId);
|
|
578
|
+
}
|
|
579
|
+
return created.data;
|
|
580
|
+
}
|
|
581
|
+
async function readPackageJsonFile(packageJsonPath) {
|
|
582
|
+
try {
|
|
583
|
+
const raw = JSON.parse(await readFile2(packageJsonPath, "utf8"));
|
|
584
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
585
|
+
throw new Error(`Workflow package "${packageJsonPath}" must contain a JSON object.`);
|
|
586
|
+
}
|
|
587
|
+
return raw;
|
|
588
|
+
} catch (error) {
|
|
589
|
+
if (error instanceof Error) {
|
|
590
|
+
throw error;
|
|
591
|
+
}
|
|
592
|
+
throw new Error(`Unable to read workflow package "${packageJsonPath}".`);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
function readRequiredPackageString(packageJson, key) {
|
|
596
|
+
const value = packageJson[key];
|
|
597
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
598
|
+
throw new Error(`Workflow package.json must include string "${key}".`);
|
|
599
|
+
}
|
|
600
|
+
return value.trim();
|
|
601
|
+
}
|
|
602
|
+
async function writeWorkflowPackageId(packageJsonPath, workflowId) {
|
|
603
|
+
const packageJson = await readPackageJsonFile(packageJsonPath);
|
|
604
|
+
await writeFile2(
|
|
605
|
+
packageJsonPath,
|
|
606
|
+
`${JSON.stringify({
|
|
607
|
+
...packageJson,
|
|
608
|
+
id: workflowId
|
|
609
|
+
}, null, 2)}
|
|
610
|
+
`
|
|
611
|
+
);
|
|
612
|
+
}
|
|
613
|
+
async function uploadPackage(options) {
|
|
614
|
+
const archiveBuffer = await readFile2(options.archivePath);
|
|
615
|
+
const archiveName = path3.basename(options.archivePath);
|
|
616
|
+
const response = await fetch(
|
|
617
|
+
`${trimTrailingSlash(options.server)}/api/workflows/${encodeURIComponent(options.workflowId)}/package?fileName=${encodeURIComponent(archiveName)}`,
|
|
618
|
+
{
|
|
619
|
+
method: "POST",
|
|
620
|
+
headers: {
|
|
621
|
+
authorization: `Bearer ${options.token}`,
|
|
622
|
+
"content-type": "application/octet-stream",
|
|
623
|
+
"x-workflow-package-name": archiveName
|
|
624
|
+
},
|
|
625
|
+
body: new Blob([archiveBuffer])
|
|
626
|
+
}
|
|
627
|
+
);
|
|
628
|
+
return response.json();
|
|
629
|
+
}
|
|
630
|
+
async function waitForPreparation(options) {
|
|
631
|
+
let previousStage = "";
|
|
632
|
+
while (true) {
|
|
633
|
+
const response = await fetchApi({
|
|
634
|
+
server: options.server,
|
|
635
|
+
token: options.token,
|
|
636
|
+
path: `/api/workflows/${encodeURIComponent(options.workflowId)}/preparations/${encodeURIComponent(options.jobId)}`
|
|
637
|
+
});
|
|
638
|
+
if (response.errCode !== 0 || response.data === void 0) {
|
|
639
|
+
return response;
|
|
640
|
+
}
|
|
641
|
+
if (response.data.stage !== previousStage) {
|
|
642
|
+
previousStage = response.data.stage;
|
|
643
|
+
process.stderr.write(`Preparation ${response.data.status}: ${response.data.stage}
|
|
644
|
+
`);
|
|
645
|
+
}
|
|
646
|
+
if (response.data.status === "success") {
|
|
647
|
+
return {
|
|
648
|
+
errCode: 0,
|
|
649
|
+
errMessage: "",
|
|
650
|
+
data: response.data.result
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
if (response.data.status === "failed") {
|
|
654
|
+
return {
|
|
655
|
+
errCode: 500,
|
|
656
|
+
errMessage: response.data.failureReason ?? `Preparation failed during ${response.data.stage}.`,
|
|
657
|
+
data: response.data
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
await delay(options.pollIntervalMs ?? 1e3);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
async function getPreparation(options) {
|
|
664
|
+
return fetchApi({
|
|
665
|
+
server: options.server,
|
|
666
|
+
token: options.token,
|
|
667
|
+
path: `/api/workflows/${encodeURIComponent(options.workflowId)}/preparations/${encodeURIComponent(options.jobId)}`
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
async function fetchApi(options) {
|
|
671
|
+
const headers = {};
|
|
672
|
+
if (options.auth !== false) {
|
|
673
|
+
headers.authorization = `Bearer ${options.token}`;
|
|
674
|
+
}
|
|
675
|
+
if (options.body !== void 0) {
|
|
676
|
+
headers["content-type"] = "application/json";
|
|
677
|
+
}
|
|
678
|
+
const response = await fetch(`${trimTrailingSlash(options.server)}${options.path}`, {
|
|
679
|
+
method: options.method ?? "GET",
|
|
680
|
+
headers,
|
|
681
|
+
body: options.body === void 0 ? void 0 : JSON.stringify(options.body)
|
|
682
|
+
});
|
|
683
|
+
return response.json();
|
|
684
|
+
}
|
|
685
|
+
async function copyWorkflowSource(sourceDir, targetDir) {
|
|
686
|
+
await cp(sourceDir, targetDir, {
|
|
687
|
+
recursive: true,
|
|
688
|
+
filter(source) {
|
|
689
|
+
const relative = path3.relative(sourceDir, source);
|
|
690
|
+
return relative === "" || !shouldExcludePackagePath(relative);
|
|
691
|
+
}
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
async function stageWorkflowWithLocalDependencies(options) {
|
|
695
|
+
const queue = [options.workflowName];
|
|
696
|
+
const visited = /* @__PURE__ */ new Set();
|
|
697
|
+
while (queue.length > 0) {
|
|
698
|
+
const currentWorkflowName = queue.shift();
|
|
699
|
+
if (!currentWorkflowName || visited.has(currentWorkflowName)) {
|
|
700
|
+
continue;
|
|
701
|
+
}
|
|
702
|
+
visited.add(currentWorkflowName);
|
|
703
|
+
const sourceDir = currentWorkflowName === options.workflowName ? options.workflowDir : path3.join(options.workspaceRoot, currentWorkflowName);
|
|
704
|
+
if (!existsSync2(path3.join(sourceDir, "package.json"))) {
|
|
705
|
+
throw new Error(
|
|
706
|
+
`Workflow "${options.workflowName}" references local workflow "${currentWorkflowName}", but ${sourceDir} is missing package.json.`
|
|
707
|
+
);
|
|
708
|
+
}
|
|
709
|
+
await copyWorkflowSource(sourceDir, path3.join(options.stagingRoot, currentWorkflowName));
|
|
710
|
+
for (const dependency of await findLocalWorkflowDependencies(sourceDir)) {
|
|
711
|
+
if (!visited.has(dependency)) {
|
|
712
|
+
queue.push(dependency);
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
async function findLocalWorkflowDependencies(sourceDir) {
|
|
718
|
+
const discovered = /* @__PURE__ */ new Set();
|
|
719
|
+
for (const fileName of ["index.ts", "interface.ts", "globals.d.ts"]) {
|
|
720
|
+
const filePath = path3.join(sourceDir, fileName);
|
|
721
|
+
if (!existsSync2(filePath)) {
|
|
722
|
+
continue;
|
|
723
|
+
}
|
|
724
|
+
const source = await readFile2(filePath, "utf8");
|
|
725
|
+
for (const match of source.matchAll(/(?:from\s+|reference path=)\"(\.\.\/([^/\"']+)(?:\/[^\"']*)?)\"/g)) {
|
|
726
|
+
const workflowName = match[2]?.trim();
|
|
727
|
+
if (workflowName) {
|
|
728
|
+
discovered.add(workflowName);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
for (const match of source.matchAll(/(?:from\s+|reference path=)'(\.\.\/([^/'"]+)(?:\/[^'"]*)?)'/g)) {
|
|
732
|
+
const workflowName = match[2]?.trim();
|
|
733
|
+
if (workflowName) {
|
|
734
|
+
discovered.add(workflowName);
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
return [...discovered].sort((left, right) => left.localeCompare(right));
|
|
739
|
+
}
|
|
740
|
+
function shouldExcludePackagePath(relativePath) {
|
|
741
|
+
const parts = relativePath.split(/[\\/]/);
|
|
742
|
+
const name = parts.at(-1) ?? "";
|
|
743
|
+
const excludedDirectories = /* @__PURE__ */ new Set([
|
|
744
|
+
"node_modules",
|
|
745
|
+
"dist",
|
|
746
|
+
"build",
|
|
747
|
+
"out",
|
|
748
|
+
"coverage",
|
|
749
|
+
".git",
|
|
750
|
+
".hg",
|
|
751
|
+
".svn",
|
|
752
|
+
".cache",
|
|
753
|
+
".turbo",
|
|
754
|
+
".next",
|
|
755
|
+
".nuxt",
|
|
756
|
+
".pnpm-store"
|
|
757
|
+
]);
|
|
758
|
+
if (parts.some((part) => excludedDirectories.has(part))) {
|
|
759
|
+
return true;
|
|
760
|
+
}
|
|
761
|
+
if (name === ".env" || name.startsWith(".env.")) {
|
|
762
|
+
return name !== ".env.example";
|
|
763
|
+
}
|
|
764
|
+
return name === ".DS_Store" || name === "Thumbs.db" || name.endsWith(".tmp") || name.endsWith(".log") || name.endsWith(".tsbuildinfo") || name.endsWith("~");
|
|
765
|
+
}
|
|
766
|
+
function normalizeWorkflowFilePath(filePath) {
|
|
767
|
+
const normalized = filePath.replaceAll("\\", "/").replace(/^\.\/+/, "");
|
|
768
|
+
if (normalized === "" || normalized.startsWith("/") || /^[a-zA-Z]:/.test(normalized) || normalized.split("/").some((part) => part === "" || part === "." || part === "..") || shouldExcludePackagePath(normalized)) {
|
|
769
|
+
throw new Error(`Invalid workflow file path "${filePath}".`);
|
|
770
|
+
}
|
|
771
|
+
return normalized;
|
|
772
|
+
}
|
|
773
|
+
function parseArgs(argv) {
|
|
774
|
+
const [command = "", ...rest] = argv;
|
|
775
|
+
const positional = [];
|
|
776
|
+
const authConfig = readCliAuthConfigSync();
|
|
777
|
+
const options = {
|
|
778
|
+
server: process.env.WORKFLOW_SERVER_URL ?? authConfig.serverUrl ?? DEFAULT_SERVER_URL,
|
|
779
|
+
token: process.env.WORKFLOW_SERVER_ADMIN_KEY ?? authConfig.apiKey ?? ""
|
|
780
|
+
};
|
|
781
|
+
for (let index = 0; index < rest.length; index += 1) {
|
|
782
|
+
const arg = rest[index];
|
|
783
|
+
switch (arg) {
|
|
784
|
+
case "--server":
|
|
785
|
+
options.server = requireValue(rest, index += 1, "--server");
|
|
786
|
+
break;
|
|
787
|
+
case "--token":
|
|
788
|
+
options.token = requireValue(rest, index += 1, "--token");
|
|
789
|
+
break;
|
|
790
|
+
case "--target":
|
|
791
|
+
options.target = requireValue(rest, index += 1, "--target");
|
|
792
|
+
break;
|
|
793
|
+
case "--version":
|
|
794
|
+
throw new Error("--version is no longer supported. Publish versions are managed by the system.");
|
|
795
|
+
case "--release-log":
|
|
796
|
+
options.releaseLog = requireValue(rest, index += 1, "--release-log");
|
|
797
|
+
break;
|
|
798
|
+
case "--file":
|
|
799
|
+
options.file = requireValue(rest, index += 1, "--file");
|
|
800
|
+
break;
|
|
801
|
+
case "--output":
|
|
802
|
+
options.output = requireValue(rest, index += 1, "--output");
|
|
803
|
+
break;
|
|
804
|
+
case "--path":
|
|
805
|
+
options.path = requireValue(rest, index += 1, "--path");
|
|
806
|
+
break;
|
|
807
|
+
case "--create":
|
|
808
|
+
options.create = true;
|
|
809
|
+
break;
|
|
810
|
+
case "--no-wait":
|
|
811
|
+
options.noWait = true;
|
|
812
|
+
break;
|
|
813
|
+
case "--source-mode": {
|
|
814
|
+
const value = requireValue(rest, index += 1, "--source-mode");
|
|
815
|
+
if (value !== "bundled" && value !== "source") {
|
|
816
|
+
throw new Error('--source-mode must be "bundled" or "source".');
|
|
817
|
+
}
|
|
818
|
+
options.sourceMode = value;
|
|
819
|
+
break;
|
|
820
|
+
}
|
|
821
|
+
default:
|
|
822
|
+
positional.push(arg);
|
|
823
|
+
break;
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
if (command === "upload" && options.noWait === true && options.releaseLog !== void 0) {
|
|
827
|
+
throw new Error("--no-wait cannot be combined with --release-log because publishing requires a prepared draft.");
|
|
828
|
+
}
|
|
829
|
+
return {
|
|
830
|
+
command,
|
|
831
|
+
positional,
|
|
832
|
+
options
|
|
833
|
+
};
|
|
834
|
+
}
|
|
835
|
+
function requireLogin(parsed) {
|
|
836
|
+
if (parsed.options.server.trim() === "" || parsed.options.token.trim() === "") {
|
|
837
|
+
throw new Error("Not logged in. Run: workspace login --server <url> or pass --token / WORKFLOW_SERVER_ADMIN_KEY.");
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
function requirePositional(parsed, index, name) {
|
|
841
|
+
const value = parsed.positional[index];
|
|
842
|
+
if (value === void 0 || value.trim() === "") {
|
|
843
|
+
throw new Error(`Missing ${name}.`);
|
|
844
|
+
}
|
|
845
|
+
return value;
|
|
846
|
+
}
|
|
847
|
+
function requireValue(argv, index, name) {
|
|
848
|
+
const value = argv[index];
|
|
849
|
+
if (value === void 0 || value.trim() === "") {
|
|
850
|
+
throw new Error(`Missing value for ${name}.`);
|
|
851
|
+
}
|
|
852
|
+
return value;
|
|
853
|
+
}
|
|
854
|
+
function runCommand(command, args, options = {}) {
|
|
855
|
+
return new Promise((resolve) => {
|
|
856
|
+
const child = spawn(command, args, {
|
|
857
|
+
cwd: options.cwd,
|
|
858
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
859
|
+
});
|
|
860
|
+
const stdout = [];
|
|
861
|
+
const stderr = [];
|
|
862
|
+
child.stdout.on("data", (chunk) => stdout.push(chunk));
|
|
863
|
+
child.stderr.on("data", (chunk) => stderr.push(chunk));
|
|
864
|
+
child.on("error", (error) => {
|
|
865
|
+
resolve({
|
|
866
|
+
exitCode: 1,
|
|
867
|
+
stdout: Buffer.concat(stdout).toString("utf8"),
|
|
868
|
+
stderr: `${Buffer.concat(stderr).toString("utf8")}${error.message}`
|
|
869
|
+
});
|
|
870
|
+
});
|
|
871
|
+
child.on("close", (exitCode) => {
|
|
872
|
+
resolve({
|
|
873
|
+
exitCode: exitCode ?? 1,
|
|
874
|
+
stdout: Buffer.concat(stdout).toString("utf8"),
|
|
875
|
+
stderr: Buffer.concat(stderr).toString("utf8")
|
|
876
|
+
});
|
|
877
|
+
});
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
function printApiResponse(response) {
|
|
881
|
+
console.log(JSON.stringify(response, null, 2));
|
|
882
|
+
}
|
|
883
|
+
function assertApiOk(response) {
|
|
884
|
+
if (response.errCode !== 0) {
|
|
885
|
+
if (response.errCode === 403) {
|
|
886
|
+
process.stderr.write(`${formatForbiddenMessage(response)}
|
|
887
|
+
`);
|
|
888
|
+
}
|
|
889
|
+
process.exitCode = 1;
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
function formatForbiddenMessage(response) {
|
|
893
|
+
const message = response.errMessage.toLowerCase();
|
|
894
|
+
if (message.includes("blocked from running")) {
|
|
895
|
+
return "403: \u5F53\u524D\u8D26\u53F7\u5DF2\u88AB\u8BE5\u9879\u76EE\u7981\u6B62\u8FD0\u884C\u3002";
|
|
896
|
+
}
|
|
897
|
+
if (message.includes("permission denied")) {
|
|
898
|
+
return "403: \u5F53\u524D\u8D26\u53F7\u5BF9\u8BE5\u9879\u76EE\u6CA1\u6709\u8DB3\u591F\u6743\u9650\u3002";
|
|
899
|
+
}
|
|
900
|
+
return `403: ${response.errMessage}`;
|
|
901
|
+
}
|
|
902
|
+
function printUsage() {
|
|
903
|
+
console.log([
|
|
904
|
+
"Usage: workflow-code workspace <command> [args]",
|
|
905
|
+
"",
|
|
906
|
+
"Local repo wrapper:",
|
|
907
|
+
" pnpm -C workspace workspace <command> [args]",
|
|
908
|
+
"",
|
|
909
|
+
"Commands:",
|
|
910
|
+
" login --server <url>",
|
|
911
|
+
" logout",
|
|
912
|
+
" status",
|
|
913
|
+
" health",
|
|
914
|
+
" pack <workflow> [--path <workflow-dir>] [--output <file.tgz>]",
|
|
915
|
+
" upload <workflow> [--path <workflow-dir>] [--file <archive>] [--release-log <text>] [--source-mode bundled|source] [--no-wait] [--create]",
|
|
916
|
+
" preparation <workflow-id> <job-id>",
|
|
917
|
+
" download <workflow> [--target draft|latest|version] [--path <dir>]",
|
|
918
|
+
" publish <workflow> [--release-log <text>] [--source-mode bundled|source]",
|
|
919
|
+
" run <workflow> [...args] [--target draft|latest|version]",
|
|
920
|
+
" debug-node <workflow> <node> [...args] [--target draft|latest|version]",
|
|
921
|
+
" versions <workflow>",
|
|
922
|
+
"",
|
|
923
|
+
"Options:",
|
|
924
|
+
" --server <url> Defaults to WORKFLOW_SERVER_URL, saved CLI auth, or http://localhost:7125",
|
|
925
|
+
" --token <token> Optional manual override. Defaults to WORKFLOW_SERVER_ADMIN_KEY or saved CLI auth API key"
|
|
926
|
+
].join("\n"));
|
|
927
|
+
}
|
|
928
|
+
function resolveRepoRoot(startDir = process.cwd()) {
|
|
929
|
+
if (process.env.WORKFLOW_REPO_ROOT !== void 0) {
|
|
930
|
+
return path3.resolve(process.env.WORKFLOW_REPO_ROOT);
|
|
931
|
+
}
|
|
932
|
+
for (const candidate of [startDir, CLI_MODULE_DIR]) {
|
|
933
|
+
const repoRoot = findRepoRoot2(candidate);
|
|
934
|
+
if (repoRoot !== void 0) {
|
|
935
|
+
return repoRoot;
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
return process.cwd();
|
|
939
|
+
}
|
|
940
|
+
function findRepoRoot2(startDir) {
|
|
941
|
+
let current = path3.resolve(startDir);
|
|
942
|
+
while (true) {
|
|
943
|
+
if (isRepoRoot(current)) {
|
|
944
|
+
return current;
|
|
945
|
+
}
|
|
946
|
+
const parent = path3.dirname(current);
|
|
947
|
+
if (parent === current) {
|
|
948
|
+
return void 0;
|
|
949
|
+
}
|
|
950
|
+
current = parent;
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
function isRepoRoot(candidate) {
|
|
954
|
+
return existsSync2(path3.join(candidate, "pnpm-workspace.yaml")) && existsSync2(path3.join(candidate, "package.json")) && existsSync2(path3.join(candidate, "workspace", "package.json")) && existsSync2(path3.join(candidate, "packages", "cli", "package.json"));
|
|
955
|
+
}
|
|
956
|
+
function trimTrailingSlash(value) {
|
|
957
|
+
return value.replace(/\/+$/, "");
|
|
958
|
+
}
|
|
959
|
+
function createTimestamp() {
|
|
960
|
+
const date = /* @__PURE__ */ new Date();
|
|
961
|
+
const pad = (value) => String(value).padStart(2, "0");
|
|
962
|
+
return [
|
|
963
|
+
date.getUTCFullYear(),
|
|
964
|
+
pad(date.getUTCMonth() + 1),
|
|
965
|
+
pad(date.getUTCDate()),
|
|
966
|
+
"-",
|
|
967
|
+
pad(date.getUTCHours()),
|
|
968
|
+
pad(date.getUTCMinutes()),
|
|
969
|
+
pad(date.getUTCSeconds())
|
|
970
|
+
].join("");
|
|
971
|
+
}
|
|
972
|
+
async function waitForDeviceToken(options) {
|
|
973
|
+
const expiresAtMs = new Date(options.expiresAt).getTime();
|
|
974
|
+
const pollMs = Math.max(1e3, options.intervalSeconds * 1e3);
|
|
975
|
+
while (!Number.isFinite(expiresAtMs) || Date.now() < expiresAtMs) {
|
|
976
|
+
const response = await fetchApi({
|
|
977
|
+
server: options.server,
|
|
978
|
+
token: "",
|
|
979
|
+
path: "/api/auth/device/token",
|
|
980
|
+
method: "POST",
|
|
981
|
+
body: { deviceCode: options.deviceCode },
|
|
982
|
+
auth: false
|
|
983
|
+
});
|
|
984
|
+
if (response.errCode === 0) {
|
|
985
|
+
return response;
|
|
986
|
+
}
|
|
987
|
+
if (response.errMessage === "slow_down") {
|
|
988
|
+
const retryAfterSeconds = readRetryAfterSeconds(response.data) ?? Math.max(options.intervalSeconds * 2, 5);
|
|
989
|
+
await delay(retryAfterSeconds * 1e3);
|
|
990
|
+
continue;
|
|
991
|
+
}
|
|
992
|
+
if (response.errMessage !== "authorization_pending") {
|
|
993
|
+
return response;
|
|
994
|
+
}
|
|
995
|
+
await delay(pollMs);
|
|
996
|
+
}
|
|
997
|
+
return {
|
|
998
|
+
errCode: 409,
|
|
999
|
+
errMessage: "expired_token"
|
|
1000
|
+
};
|
|
1001
|
+
}
|
|
1002
|
+
async function openExternalBrowser(url) {
|
|
1003
|
+
const commands = process.platform === "darwin" ? [["open", url]] : process.platform === "win32" ? [["cmd", "/c", "start", "", url]] : [["xdg-open", url]];
|
|
1004
|
+
for (const [command, ...args] of commands) {
|
|
1005
|
+
const result = await runCommand(command, args);
|
|
1006
|
+
if (result.exitCode === 0) {
|
|
1007
|
+
return;
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
function delay(ms) {
|
|
1012
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1013
|
+
}
|
|
1014
|
+
function readRetryAfterSeconds(data) {
|
|
1015
|
+
if (typeof data !== "object" || data === null) {
|
|
1016
|
+
return void 0;
|
|
1017
|
+
}
|
|
1018
|
+
const value = data.retryAfterSeconds;
|
|
1019
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : void 0;
|
|
1020
|
+
}
|
|
1021
|
+
if (process.argv[1] !== void 0 && import.meta.url === pathToFileURL(path3.resolve(process.argv[1])).href) {
|
|
1022
|
+
main().catch((error) => {
|
|
1023
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
1024
|
+
process.exitCode = 1;
|
|
1025
|
+
});
|
|
1026
|
+
}
|
|
1027
|
+
export {
|
|
1028
|
+
ensureWorkflowProjectCreated,
|
|
1029
|
+
findLocalWorkflowDependencies,
|
|
1030
|
+
formatForbiddenMessage,
|
|
1031
|
+
getPreparation,
|
|
1032
|
+
main,
|
|
1033
|
+
normalizeWorkflowFilePath,
|
|
1034
|
+
packWorkflow,
|
|
1035
|
+
parseArgs,
|
|
1036
|
+
readPackageJsonFile,
|
|
1037
|
+
readWorkflowPackageContext,
|
|
1038
|
+
resolveRemoteWorkflowId,
|
|
1039
|
+
resolveRepoRoot,
|
|
1040
|
+
stageWorkflowWithLocalDependencies,
|
|
1041
|
+
waitForDeviceToken,
|
|
1042
|
+
waitForPreparation,
|
|
1043
|
+
writeWorkflowPackageId
|
|
1044
|
+
};
|