@offsida/cli 0.2.0 → 0.2.1
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/api.d.ts +5 -2
- package/dist/api.js +28 -11
- package/dist/auth.d.ts +1 -0
- package/dist/auth.js +13 -2
- package/dist/bundle.js +2 -0
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +5 -1
- package/dist/push-directory.js +1 -1
- package/dist/push.d.ts +1 -0
- package/dist/push.js +7 -1
- package/dist/resolve-package.d.ts +1 -1
- package/dist/resolve-package.js +7 -4
- package/dist/workflows.js +6 -6
- package/package.json +2 -2
package/dist/api.d.ts
CHANGED
|
@@ -1,20 +1,23 @@
|
|
|
1
1
|
import { type AuthOpts } from './auth.js';
|
|
2
|
+
type Sleep = (ms: number) => Promise<void>;
|
|
2
3
|
export type Api = {
|
|
3
4
|
url: string;
|
|
4
5
|
org: string;
|
|
5
6
|
headers: Record<string, string>;
|
|
6
7
|
fetch: typeof fetch;
|
|
8
|
+
sleep: Sleep;
|
|
7
9
|
};
|
|
8
10
|
export type Res = {
|
|
9
11
|
status: number;
|
|
10
12
|
body: unknown;
|
|
11
13
|
};
|
|
12
|
-
export declare function connect(opts: AuthOpts, fetchImpl?: typeof fetch): Promise<Api>;
|
|
14
|
+
export declare function connect(opts: AuthOpts, fetchImpl?: typeof fetch, sleep?: Sleep): Promise<Api>;
|
|
13
15
|
export declare function call(api: Api, method: string, path: string, init?: {
|
|
14
16
|
body?: unknown;
|
|
15
17
|
headers?: Record<string, string>;
|
|
16
18
|
}): Promise<Res>;
|
|
17
19
|
export declare function errorCode(res: Res): string | undefined;
|
|
18
|
-
export declare function failure(operation: string, res: Res): Error;
|
|
20
|
+
export declare function failure(operation: string, res: Res, scope?: string): Error;
|
|
19
21
|
export declare function isRetryable(res: Res): boolean;
|
|
20
22
|
export declare function withRetry(send: () => Promise<Res>, sleep: (ms: number) => Promise<void>): Promise<Res>;
|
|
23
|
+
export {};
|
package/dist/api.js
CHANGED
|
@@ -1,16 +1,33 @@
|
|
|
1
1
|
import { ORG_HEADER } from '@offsida/types';
|
|
2
|
-
import { assertTarget, authHeaders } from './auth.js';
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
const
|
|
6
|
-
|
|
2
|
+
import { assertTarget, authHeaders, reach } from './auth.js';
|
|
3
|
+
const realSleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
4
|
+
export async function connect(opts, fetchImpl = fetch, sleep = realSleep) {
|
|
5
|
+
const url = opts.url.replace(/\/+$/, '');
|
|
6
|
+
assertTarget({ ...opts, url });
|
|
7
|
+
const auth = await authHeaders({ ...opts, url }, fetchImpl);
|
|
8
|
+
return { url, org: opts.org, headers: { ...auth, [ORG_HEADER]: opts.org }, fetch: fetchImpl, sleep };
|
|
7
9
|
}
|
|
10
|
+
const BACKOFF_MS = [500, 1500];
|
|
11
|
+
const MAX_RETRY_AFTER_MS = 60_000;
|
|
12
|
+
// A 429 is refused before the route runs (token-auth throttle, per-route limits), so resending is safe
|
|
13
|
+
// for every method. Waits the server's Retry-After (capped), else the backoff; three attempts in all.
|
|
8
14
|
// Never throws on status: callers branch on it. A non-JSON body (a proxy's HTML 502) reads as undefined.
|
|
9
15
|
export async function call(api, method, path, init) {
|
|
16
|
+
let res = await send(api, method, path, init);
|
|
17
|
+
for (const backoff of BACKOFF_MS) {
|
|
18
|
+
if (res.status !== 429)
|
|
19
|
+
break;
|
|
20
|
+
const after = Number(res.retryAfter);
|
|
21
|
+
await api.sleep(res.retryAfter && Number.isFinite(after) ? Math.min(after * 1000, MAX_RETRY_AFTER_MS) : backoff);
|
|
22
|
+
res = await send(api, method, path, init);
|
|
23
|
+
}
|
|
24
|
+
return { status: res.status, body: res.body };
|
|
25
|
+
}
|
|
26
|
+
async function send(api, method, path, init) {
|
|
10
27
|
const headers = { ...api.headers, ...init?.headers };
|
|
11
28
|
if (init?.body !== undefined)
|
|
12
29
|
headers['content-type'] = 'application/json';
|
|
13
|
-
const res = await api.fetch
|
|
30
|
+
const res = await reach(api.fetch, `${api.url}/api${path}`, {
|
|
14
31
|
method,
|
|
15
32
|
headers,
|
|
16
33
|
body: init?.body === undefined ? undefined : JSON.stringify(init.body),
|
|
@@ -23,7 +40,7 @@ export async function call(api, method, path, init) {
|
|
|
23
40
|
catch {
|
|
24
41
|
body = undefined;
|
|
25
42
|
}
|
|
26
|
-
return { status: res.status, body };
|
|
43
|
+
return { status: res.status, body, retryAfter: res.headers.get('retry-after') };
|
|
27
44
|
}
|
|
28
45
|
export function errorCode(res) {
|
|
29
46
|
const b = (res.body ?? {});
|
|
@@ -34,11 +51,12 @@ export function errorCode(res) {
|
|
|
34
51
|
}
|
|
35
52
|
// Reads both error envelopes: AppError `{ error: { code, message } }`, and the idempotency/validation
|
|
36
53
|
// `{ error: string, code?, field? }`.
|
|
37
|
-
export function failure(operation, res) {
|
|
54
|
+
export function failure(operation, res, scope) {
|
|
38
55
|
if (res.status === 401)
|
|
39
56
|
return new Error('auth rejected (HTTP 401) — token invalid/revoked, or login expired');
|
|
40
|
-
if (res.status === 403)
|
|
41
|
-
return new Error(`forbidden (HTTP 403) — ${operation} needs a token with the right scope`);
|
|
57
|
+
if (res.status === 403) {
|
|
58
|
+
return new Error(`forbidden (HTTP 403) — ${operation} needs a token with ${scope ? `the ${scope}` : 'the right'} scope`);
|
|
59
|
+
}
|
|
42
60
|
let message = `${operation} failed (HTTP ${res.status})`;
|
|
43
61
|
const b = (res.body ?? {});
|
|
44
62
|
if (b.error && typeof b.error === 'object') {
|
|
@@ -60,7 +78,6 @@ export function isRetryable(res) {
|
|
|
60
78
|
const e = res.body?.error;
|
|
61
79
|
return !!e && typeof e === 'object' && e.retryable === true;
|
|
62
80
|
}
|
|
63
|
-
const BACKOFF_MS = [500, 1500];
|
|
64
81
|
// Up to three attempts. The caller's `send` reuses one Idempotency-Key, so the server deduplicates.
|
|
65
82
|
export async function withRetry(send, sleep) {
|
|
66
83
|
let res = await send();
|
package/dist/auth.d.ts
CHANGED
|
@@ -8,4 +8,5 @@ export type AuthOpts = {
|
|
|
8
8
|
};
|
|
9
9
|
export declare function isLocal(url: string): boolean;
|
|
10
10
|
export declare function assertTarget(opts: AuthOpts): void;
|
|
11
|
+
export declare function reach(fetchImpl: typeof fetch, url: string, init: RequestInit): Promise<Response>;
|
|
11
12
|
export declare function authHeaders(opts: AuthOpts, fetchImpl?: typeof fetch): Promise<Record<string, string>>;
|
package/dist/auth.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export function isLocal(url) {
|
|
2
2
|
try {
|
|
3
3
|
const h = new URL(url).hostname;
|
|
4
|
-
return h === 'localhost' || h === '127.0.0.1' || h === '0.0.0.0';
|
|
4
|
+
return h === 'localhost' || h === '127.0.0.1' || h === '0.0.0.0' || h === '[::1]';
|
|
5
5
|
}
|
|
6
6
|
catch {
|
|
7
7
|
return false;
|
|
@@ -19,13 +19,24 @@ export function assertTarget(opts) {
|
|
|
19
19
|
throw new Error(`refusing to push to non-local URL ${opts.url} without --allow-remote (or OFFSIDA_ALLOW_REMOTE=1)`);
|
|
20
20
|
}
|
|
21
21
|
}
|
|
22
|
+
// fetch rejects with a bare "fetch failed"; name the URL and the socket error (ECONNREFUSED, ENOTFOUND…).
|
|
23
|
+
export async function reach(fetchImpl, url, init) {
|
|
24
|
+
try {
|
|
25
|
+
return await fetchImpl(url, init);
|
|
26
|
+
}
|
|
27
|
+
catch (e) {
|
|
28
|
+
const cause = e.cause;
|
|
29
|
+
const detail = typeof cause?.code === 'string' ? cause.code : e.message;
|
|
30
|
+
throw new Error(`cannot reach ${url} (${detail})`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
22
33
|
// Two auth modes:
|
|
23
34
|
// • token — non-interactive (CI): `Authorization: Bearer <osk_…>`, an org API token (Story 022).
|
|
24
35
|
// • email+code — interactive: exchange an emailed login code for a session cookie.
|
|
25
36
|
export async function authHeaders(opts, fetchImpl = fetch) {
|
|
26
37
|
if (opts.token)
|
|
27
38
|
return { authorization: `Bearer ${opts.token}` };
|
|
28
|
-
const verify = await fetchImpl
|
|
39
|
+
const verify = await reach(fetchImpl, `${opts.url}/api/auth/verify`, {
|
|
29
40
|
method: 'POST',
|
|
30
41
|
headers: { 'content-type': 'application/json' },
|
|
31
42
|
body: JSON.stringify({ email: opts.email, code: opts.code }),
|
package/dist/bundle.js
CHANGED
|
@@ -31,6 +31,8 @@ export async function bundleEntry(entryFile, opts) {
|
|
|
31
31
|
target: 'es2022',
|
|
32
32
|
write: false,
|
|
33
33
|
legalComments: 'none',
|
|
34
|
+
// Halves an SDK package (most of it is zod) against the platform's 1 MiB cap (push.ts CODE_SIZE_LIMIT).
|
|
35
|
+
minify: true,
|
|
34
36
|
});
|
|
35
37
|
const out = result.outputFiles?.[0]?.text;
|
|
36
38
|
if (!out)
|
package/dist/cli.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { type PushDirectoryDeps } from './push-directory.js';
|
|
3
|
-
export declare const USAGE = "usage:\n offsida push [dir] [--publish] [--url <u>] [--org <id>] [--allow-remote]\n offsida build [dir] [--out <file>]\n\ndir defaults to the current directory and must contain a package.json.\n\npush uploads the package's code (its \"exports\"/\"main\", else src/index.ts or index.ts)\nas artifact <package name without scope> (override: \"offsida\": { \"name\": \"\u2026\" }),\nthen every *.workflow.json as that workflow's draft, replacing edits made in the\nOffsida editor. A file's optional \"id\" pins one workflow in one org; without it the\nworkflow is matched by name or created. --publish also publishes each workflow;\n\"latest\" steps lock to the code just pushed.\n\nA first publish goes live with no capability confirmation (offsida.web#14). A later\npublish that adds access is left for confirming in Offsida (exit 2). Every push mints\na new code revision, so --publish republishes \"latest\" workflows (offsida.web#15).\n\nAuth (environment only): OFFSIDA_TOKEN, or OFFSIDA_EMAIL + OFFSIDA_CODE.\nScopes: code:publish; workflows:list, :get, :create, :update; workflows:publish with --publish.\nAlso: OFFSIDA_URL (default http://localhost:3808), OFFSIDA_ORG, OFFSIDA_ALLOW_REMOTE=1.\n\nLimits: name matching reads a list that lags a few seconds, so two quick pushes of a\nnew workflow can create two. Same-named workflows in different packages overwrite\neach other without \"id\". Deleting a *.workflow.json leaves its workflow live.\n\nExit codes: 0 done, 1 something failed, 2 something needs confirming in Offsida.";
|
|
3
|
+
export declare const USAGE = "usage:\n offsida push [dir] [--publish] [--url <u>] [--org <id>] [--allow-remote]\n offsida build [dir] [--out <file>]\n\ndir defaults to the current directory and must contain a package.json.\n\npush uploads the package's code (its \"exports\"/\"main\", else src/index.ts or index.ts)\nas artifact <package name without scope> (override: \"offsida\": { \"name\": \"\u2026\" }),\nthen every *.workflow.json as that workflow's draft, replacing edits made in the\nOffsida editor. Subfolders with their own package.json are separate packages and\nare skipped. A file's optional \"id\" pins one workflow in one org; without it the\nworkflow is matched by name or created. --publish also publishes each workflow;\n\"latest\" steps lock to the code just pushed.\n\nA first publish goes live with no capability confirmation (offsida.web#14). A later\npublish that adds access is left for confirming in Offsida (exit 2). Every push mints\na new code revision, so --publish republishes \"latest\" workflows (offsida.web#15).\n\nAuth (environment only): OFFSIDA_TOKEN, or OFFSIDA_EMAIL + OFFSIDA_CODE.\nScopes: code:publish; workflows:list, :get, :create, :update; workflows:publish with --publish.\nAlso: OFFSIDA_URL (default http://localhost:3808), OFFSIDA_ORG, OFFSIDA_ALLOW_REMOTE=1.\n\nLimits: name matching reads a list that lags a few seconds, so two quick pushes of a\nnew workflow can create two. Same-named workflows in different packages overwrite\neach other without \"id\". Deleting a *.workflow.json leaves its workflow live.\n\nBundles are minified and must stay under the platform's 1 MiB cap. A rate-limited\nrequest (429) waits out its Retry-After, up to a minute, and is retried.\n\nExit codes: 0 done, 1 something failed, 2 something needs confirming in Offsida.";
|
|
4
4
|
type RunDeps = PushDirectoryDeps & {
|
|
5
5
|
out?: (s: string) => void;
|
|
6
6
|
err?: (s: string) => void;
|
package/dist/cli.js
CHANGED
|
@@ -14,7 +14,8 @@ dir defaults to the current directory and must contain a package.json.
|
|
|
14
14
|
push uploads the package's code (its "exports"/"main", else src/index.ts or index.ts)
|
|
15
15
|
as artifact <package name without scope> (override: "offsida": { "name": "…" }),
|
|
16
16
|
then every *.workflow.json as that workflow's draft, replacing edits made in the
|
|
17
|
-
Offsida editor.
|
|
17
|
+
Offsida editor. Subfolders with their own package.json are separate packages and
|
|
18
|
+
are skipped. A file's optional "id" pins one workflow in one org; without it the
|
|
18
19
|
workflow is matched by name or created. --publish also publishes each workflow;
|
|
19
20
|
"latest" steps lock to the code just pushed.
|
|
20
21
|
|
|
@@ -30,6 +31,9 @@ Limits: name matching reads a list that lags a few seconds, so two quick pushes
|
|
|
30
31
|
new workflow can create two. Same-named workflows in different packages overwrite
|
|
31
32
|
each other without "id". Deleting a *.workflow.json leaves its workflow live.
|
|
32
33
|
|
|
34
|
+
Bundles are minified and must stay under the platform's 1 MiB cap. A rate-limited
|
|
35
|
+
request (429) waits out its Retry-After, up to a minute, and is retried.
|
|
36
|
+
|
|
33
37
|
Exit codes: 0 done, 1 something failed, 2 something needs confirming in Offsida.`;
|
|
34
38
|
// Each command's flags: `true` takes a value, `false` is a switch. Anything else is refused — in
|
|
35
39
|
// particular `--token`/`--code`, so a secret never has to sit in argv or shell history.
|
package/dist/push-directory.js
CHANGED
|
@@ -36,7 +36,7 @@ export async function pushDirectory(opts, deps = {}) {
|
|
|
36
36
|
workflowDeps.pinned.add(f.id);
|
|
37
37
|
if (!pkg.entry && files.length === 0)
|
|
38
38
|
throw new Error(`${opts.dir}: nothing to push — no code entry and no *.workflow.json files`);
|
|
39
|
-
const api = await connect(opts, deps.fetch);
|
|
39
|
+
const api = await connect(opts, deps.fetch, workflowDeps.sleep);
|
|
40
40
|
// Code first, so `latest` resolves to it. A failed build stops here: publishing workflows now would
|
|
41
41
|
// lock them to the previous revision.
|
|
42
42
|
if (pkg.entry) {
|
package/dist/push.d.ts
CHANGED
package/dist/push.js
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
import { basename } from 'node:path';
|
|
2
2
|
import { bundlePackage } from './bundle.js';
|
|
3
3
|
import { call, connect, failure } from './api.js';
|
|
4
|
+
// The platform's cap on an artifact's modules, counted in string length (MAX_TOTAL_MODULES_BYTES in
|
|
5
|
+
// apps/workflows/src/services/codeArtifacts.ts). Checked here so an oversized bundle fails before upload.
|
|
6
|
+
export const CODE_SIZE_LIMIT = 1024 * 1024;
|
|
4
7
|
// Upload one bundle as a new revision of artifact `name`. The build runs synchronously server-side:
|
|
5
8
|
// the response is `ready` or `failed`.
|
|
6
9
|
export async function pushCode(api, name, code) {
|
|
10
|
+
if (code.length > CODE_SIZE_LIMIT) {
|
|
11
|
+
throw new Error(`${name} bundle is ${code.length} characters; the platform cap is ${CODE_SIZE_LIMIT} (1 MiB) — trim dependencies`);
|
|
12
|
+
}
|
|
7
13
|
const res = await call(api, 'POST', '/code', { body: { name, modules: { 'index.js': code } } });
|
|
8
14
|
if (res.status === 401 || res.status === 403)
|
|
9
|
-
throw failure('pushing code', res);
|
|
15
|
+
throw failure('pushing code', res, 'code:publish');
|
|
10
16
|
const body = (res.body ?? {});
|
|
11
17
|
if (res.status !== 201 || !body.ok || !body.revision) {
|
|
12
18
|
throw new Error(`code push failed (${body.status ?? res.status}): ${body.failureReason ?? 'unknown'}`);
|
|
@@ -7,6 +7,6 @@ export type ResolvedPackage = {
|
|
|
7
7
|
/**
|
|
8
8
|
* A package directory → what `offsida push` uploads. Artifact name: `offsida.name`, else the npm name
|
|
9
9
|
* without its scope. Code entry: exports["."] → main → src/index.ts → index.ts (first that exists;
|
|
10
|
-
* null = no code). Workflows: every *.workflow.json below, skipping node_modules, dist
|
|
10
|
+
* null = no code). Workflows: every *.workflow.json below, skipping node_modules, dist, dot-dirs and nested packages.
|
|
11
11
|
*/
|
|
12
12
|
export declare function resolvePackage(dirArg: string): Promise<ResolvedPackage>;
|
package/dist/resolve-package.js
CHANGED
|
@@ -23,13 +23,16 @@ function exportsEntry(exp) {
|
|
|
23
23
|
}
|
|
24
24
|
return undefined;
|
|
25
25
|
}
|
|
26
|
+
// A subdirectory with its own package.json is another package: its workflows go with its own push, not
|
|
27
|
+
// this one's (pushing them from a monorepo root would upload them without their code).
|
|
26
28
|
async function findWorkflowFiles(dir) {
|
|
27
29
|
const out = [];
|
|
28
30
|
for (const e of await readdir(dir, { withFileTypes: true })) {
|
|
29
31
|
const path = join(dir, e.name);
|
|
30
32
|
if (e.isDirectory()) {
|
|
31
|
-
if (
|
|
32
|
-
|
|
33
|
+
if (SKIP_DIRS.has(e.name) || e.name.startsWith('.') || (await isFile(join(path, 'package.json'))))
|
|
34
|
+
continue;
|
|
35
|
+
out.push(...(await findWorkflowFiles(path)));
|
|
33
36
|
}
|
|
34
37
|
else if (e.isFile() && e.name.endsWith('.workflow.json'))
|
|
35
38
|
out.push(path);
|
|
@@ -39,7 +42,7 @@ async function findWorkflowFiles(dir) {
|
|
|
39
42
|
/**
|
|
40
43
|
* A package directory → what `offsida push` uploads. Artifact name: `offsida.name`, else the npm name
|
|
41
44
|
* without its scope. Code entry: exports["."] → main → src/index.ts → index.ts (first that exists;
|
|
42
|
-
* null = no code). Workflows: every *.workflow.json below, skipping node_modules, dist
|
|
45
|
+
* null = no code). Workflows: every *.workflow.json below, skipping node_modules, dist, dot-dirs and nested packages.
|
|
43
46
|
*/
|
|
44
47
|
export async function resolvePackage(dirArg) {
|
|
45
48
|
const dir = resolve(dirArg);
|
|
@@ -49,7 +52,7 @@ export async function resolvePackage(dirArg) {
|
|
|
49
52
|
raw = await readFile(pkgPath, 'utf8');
|
|
50
53
|
}
|
|
51
54
|
catch {
|
|
52
|
-
throw new Error(`${dirArg}: no package.json — offsida
|
|
55
|
+
throw new Error(`${dirArg}: no package.json — run offsida in a package directory`);
|
|
53
56
|
}
|
|
54
57
|
let pkg;
|
|
55
58
|
try {
|
package/dist/workflows.js
CHANGED
|
@@ -12,7 +12,7 @@ export async function listWorkflows(api) {
|
|
|
12
12
|
for (;;) {
|
|
13
13
|
const res = await call(api, 'GET', `/workflows?limit=${PAGE}${cursor === undefined ? '' : `&cursor=${cursor}`}`);
|
|
14
14
|
if (res.status !== 200)
|
|
15
|
-
throw failure('listing workflows', res);
|
|
15
|
+
throw failure('listing workflows', res, 'workflows:list');
|
|
16
16
|
const items = (res.body?.items ?? []);
|
|
17
17
|
const before = rows.size;
|
|
18
18
|
for (const r of items)
|
|
@@ -41,7 +41,7 @@ export async function findWorkflow(api, file, rows, pinned = new Set()) {
|
|
|
41
41
|
throw new Error(`no workflow ${file.id} in this org — "id" ties a file to one org; fix it, or remove it to match by name`);
|
|
42
42
|
}
|
|
43
43
|
if (res.status !== 200 || !wf)
|
|
44
|
-
throw failure('reading workflow', res);
|
|
44
|
+
throw failure('reading workflow', res, 'workflows:get');
|
|
45
45
|
return { kind: 'existing', id: wf.id, currentName: wf.name };
|
|
46
46
|
}
|
|
47
47
|
const matches = rows.filter((r) => r.name === file.name);
|
|
@@ -57,7 +57,7 @@ export async function findWorkflow(api, file, rows, pinned = new Set()) {
|
|
|
57
57
|
}
|
|
58
58
|
const { res, wf } = await readWorkflow(api, id);
|
|
59
59
|
if (res.status !== 200 || !wf)
|
|
60
|
-
throw failure('reading workflow', res);
|
|
60
|
+
throw failure('reading workflow', res, 'workflows:get');
|
|
61
61
|
if (wf.name !== file.name)
|
|
62
62
|
throw new Error(`workflow ${id} changed while resolving "${file.name}" — retry`);
|
|
63
63
|
return { kind: 'existing', id: wf.id, currentName: wf.name };
|
|
@@ -90,12 +90,12 @@ async function saveAndPublish(api, file, rows, publish, deps, say) {
|
|
|
90
90
|
const key = deps.idempotencyKey();
|
|
91
91
|
res = await withRetry(() => call(api, 'POST', '/workflows', { body, headers: { 'Idempotency-Key': key } }), deps.sleep);
|
|
92
92
|
if (res.status !== 201)
|
|
93
|
-
throw failure('saving workflow', res);
|
|
93
|
+
throw failure('saving workflow', res, 'workflows:create');
|
|
94
94
|
}
|
|
95
95
|
else {
|
|
96
96
|
res = await call(api, 'PUT', `/workflows/${encodeURIComponent(target.id)}`, { body });
|
|
97
97
|
if (res.status !== 200)
|
|
98
|
-
throw failure('saving workflow', res);
|
|
98
|
+
throw failure('saving workflow', res, 'workflows:update');
|
|
99
99
|
}
|
|
100
100
|
const saved = res.body;
|
|
101
101
|
const id = saved.workflow.id;
|
|
@@ -144,5 +144,5 @@ async function saveAndPublish(api, file, rows, publish, deps, say) {
|
|
|
144
144
|
say(`confirm and publish at ${editorUrl}`);
|
|
145
145
|
return 'confirm';
|
|
146
146
|
}
|
|
147
|
-
throw failure('publishing workflow', res);
|
|
147
|
+
throw failure('publishing workflow', res, 'workflows:publish');
|
|
148
148
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@offsida/cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "The `offsida` CLI — bundle an integration package and publish it to a running Offsida environment.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
39
|
"esbuild-wasm": "^0.25.12",
|
|
40
|
-
"@offsida/types": "0.
|
|
40
|
+
"@offsida/types": "0.2.0"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@types/node": "^25.9.3",
|