jitsu-cli 1.10.3 → 1.11.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/bin/jitsu +3 -0
- package/build.mts +39 -0
- package/compiled/package.json +27 -29
- package/compiled/src/commands/build.js +43 -55
- package/compiled/src/commands/config/handlers.js +292 -0
- package/compiled/src/commands/config/index.js +148 -0
- package/compiled/src/commands/config/resources.js +88 -0
- package/compiled/src/commands/default-workspace.js +39 -0
- package/compiled/src/commands/deploy.js +127 -78
- package/compiled/src/commands/init.js +17 -21
- package/compiled/src/commands/login.js +24 -27
- package/compiled/src/commands/shared.js +12 -17
- package/compiled/src/commands/spec.js +31 -0
- package/compiled/src/commands/test.js +7 -10
- package/compiled/src/commands/whoami.js +8 -12
- package/compiled/src/index.js +40 -24
- package/compiled/src/lib/api-client.js +52 -0
- package/compiled/src/lib/auth-file.js +67 -0
- package/compiled/src/lib/body-builder.js +53 -0
- package/compiled/src/lib/body-fields.js +47 -0
- package/compiled/src/lib/chalk-code-highlight.js +15 -20
- package/compiled/src/lib/compiled-function.js +27 -29
- package/compiled/src/lib/dotted.js +34 -0
- package/compiled/src/lib/indent.js +3 -8
- package/compiled/src/lib/project-config.js +20 -0
- package/compiled/src/lib/renderer.js +33 -0
- package/compiled/src/lib/spec.js +11 -0
- package/compiled/src/lib/template.js +7 -11
- package/compiled/src/lib/version.js +13 -20
- package/compiled/src/templates/functions.js +13 -18
- package/dist/main.js +45496 -86270
- package/dist/main.js.map +7 -1
- package/package.json +27 -29
- package/src/commands/build.ts +22 -27
- package/src/commands/config/handlers.ts +339 -0
- package/src/commands/config/index.ts +171 -0
- package/src/commands/config/resources.ts +110 -0
- package/src/commands/default-workspace.ts +44 -0
- package/src/commands/deploy.ts +145 -44
- package/src/commands/login.ts +3 -1
- package/src/commands/spec.ts +32 -0
- package/src/index.ts +36 -19
- package/src/lib/api-client.ts +64 -0
- package/src/lib/auth-file.ts +83 -0
- package/src/lib/body-builder.ts +68 -0
- package/src/lib/body-fields.ts +61 -0
- package/src/lib/compiled-function.ts +30 -23
- package/src/lib/dotted.ts +43 -0
- package/src/lib/project-config.ts +32 -0
- package/src/lib/renderer.ts +44 -0
- package/src/lib/spec.ts +32 -0
- package/tsconfig.json +2 -19
- package/.turbo/turbo-build.log +0 -28
- package/.turbo/turbo-clean.log +0 -5
- package/babel.config.cjs +0 -4
- package/dist/140.js +0 -452
- package/dist/140.js.map +0 -1
- package/dist/233.js +0 -4890
- package/dist/233.js.map +0 -1
- package/dist/445e7f36f8a19c2bf682.js +0 -900
- package/webpack.config.js +0 -49
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// Single source of truth for the `jitsu config <noun>` command tree.
|
|
2
|
+
// `kind` selects which verb handlers apply; `type` is the segment used in
|
|
3
|
+
// /api/{workspaceId}/config/{type} for standard config objects.
|
|
4
|
+
|
|
5
|
+
export type ResourceKind = "configObject" | "workspace" | "link" | "profile-builder";
|
|
6
|
+
|
|
7
|
+
export type Resource = {
|
|
8
|
+
noun: string;
|
|
9
|
+
aliases: string[];
|
|
10
|
+
kind: ResourceKind;
|
|
11
|
+
type?: string;
|
|
12
|
+
supportsTest?: boolean;
|
|
13
|
+
// Description used in `--help`.
|
|
14
|
+
description: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const resources: Resource[] = [
|
|
18
|
+
{
|
|
19
|
+
noun: "workspaces",
|
|
20
|
+
aliases: ["workspace"],
|
|
21
|
+
kind: "workspace",
|
|
22
|
+
description: "Workspaces accessible to the current user",
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
noun: "destinations",
|
|
26
|
+
aliases: ["destination", "dest"],
|
|
27
|
+
kind: "configObject",
|
|
28
|
+
type: "destination",
|
|
29
|
+
supportsTest: true,
|
|
30
|
+
description: "Destinations (warehouses, databases, services receiving events)",
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
noun: "streams",
|
|
34
|
+
aliases: ["stream"],
|
|
35
|
+
kind: "configObject",
|
|
36
|
+
type: "stream",
|
|
37
|
+
supportsTest: true,
|
|
38
|
+
description: "Event streams (formerly known as sources)",
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
noun: "functions",
|
|
42
|
+
aliases: ["function", "fn"],
|
|
43
|
+
kind: "configObject",
|
|
44
|
+
type: "function",
|
|
45
|
+
description: "User-defined functions (UDFs). For dev workflow see `jitsu deploy`.",
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
noun: "services",
|
|
49
|
+
aliases: ["service"],
|
|
50
|
+
kind: "configObject",
|
|
51
|
+
type: "service",
|
|
52
|
+
supportsTest: true,
|
|
53
|
+
description: "External connector services (Airbyte protocol)",
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
noun: "domains",
|
|
57
|
+
aliases: ["domain"],
|
|
58
|
+
kind: "configObject",
|
|
59
|
+
type: "domain",
|
|
60
|
+
description: "Custom ingestion domains",
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
noun: "misc",
|
|
64
|
+
aliases: [],
|
|
65
|
+
kind: "configObject",
|
|
66
|
+
type: "misc",
|
|
67
|
+
description: "Miscellaneous configuration entities (free-form)",
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
noun: "notifications",
|
|
71
|
+
aliases: ["notification"],
|
|
72
|
+
kind: "configObject",
|
|
73
|
+
type: "notification",
|
|
74
|
+
description: "Alert channels (email/Slack)",
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
noun: "connections",
|
|
78
|
+
aliases: ["connection", "link", "links"],
|
|
79
|
+
kind: "link",
|
|
80
|
+
description: "Connections between streams/services and destinations",
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
noun: "profile-builders",
|
|
84
|
+
aliases: ["profile-builder"],
|
|
85
|
+
kind: "profile-builder",
|
|
86
|
+
description: "Profile builders (identity stitching)",
|
|
87
|
+
},
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
export type Verb = "list" | "get" | "create" | "update" | "delete" | "test";
|
|
91
|
+
|
|
92
|
+
// Which verbs apply to a given resource kind.
|
|
93
|
+
export function verbsFor(kind: ResourceKind): Verb[] {
|
|
94
|
+
switch (kind) {
|
|
95
|
+
case "configObject":
|
|
96
|
+
return ["list", "get", "create", "update", "delete"];
|
|
97
|
+
case "workspace":
|
|
98
|
+
return ["list", "get", "create", "update", "delete"];
|
|
99
|
+
case "link":
|
|
100
|
+
// link has no per-id GET; list+create(upsert)+update(alias)+delete
|
|
101
|
+
return ["list", "create", "update", "delete"];
|
|
102
|
+
case "profile-builder":
|
|
103
|
+
return ["list", "create", "update", "delete"];
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function findResource(name: string): Resource | undefined {
|
|
108
|
+
const lower = name.toLowerCase();
|
|
109
|
+
return resources.find(r => r.noun === lower || r.aliases.includes(lower));
|
|
110
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { ApiClient, ApiError } from "../lib/api-client";
|
|
2
|
+
import { authFilePath, readAuthFile, resolveAuth, updateAuthFile } from "../lib/auth-file";
|
|
3
|
+
import { red } from "../lib/chalk-code-highlight";
|
|
4
|
+
|
|
5
|
+
export type DefaultWorkspaceOpts = {
|
|
6
|
+
host?: string;
|
|
7
|
+
apikey?: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export async function setDefaultWorkspace(idOrSlug: string, opts: DefaultWorkspaceOpts) {
|
|
11
|
+
try {
|
|
12
|
+
const auth = resolveAuth(opts);
|
|
13
|
+
const client = new ApiClient(auth);
|
|
14
|
+
let workspace: { id: string; slug?: string; name?: string };
|
|
15
|
+
try {
|
|
16
|
+
workspace = await client.request({
|
|
17
|
+
method: "GET",
|
|
18
|
+
path: `/api/workspace/${encodeURIComponent(idOrSlug)}`,
|
|
19
|
+
});
|
|
20
|
+
} catch (e) {
|
|
21
|
+
if (e instanceof ApiError && e.status === 404) {
|
|
22
|
+
throw new Error(`Workspace '${idOrSlug}' not found (or you don't have access)`);
|
|
23
|
+
}
|
|
24
|
+
throw e;
|
|
25
|
+
}
|
|
26
|
+
updateAuthFile({ defaultWorkspace: workspace.id });
|
|
27
|
+
const label = workspace.slug ? `${workspace.slug} (${workspace.id})` : workspace.id;
|
|
28
|
+
console.log(`Default workspace set to ${label}.`);
|
|
29
|
+
console.log(`Saved to ${authFilePath()}.`);
|
|
30
|
+
} catch (e) {
|
|
31
|
+
console.error(red(`Error: ${e instanceof Error ? e.message : String(e)}`));
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function unsetDefaultWorkspace() {
|
|
37
|
+
const file = readAuthFile();
|
|
38
|
+
if (!file?.defaultWorkspace) {
|
|
39
|
+
console.log("No default workspace is set.");
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
updateAuthFile({ defaultWorkspace: undefined });
|
|
43
|
+
console.log("Default workspace unset.");
|
|
44
|
+
}
|
package/src/commands/deploy.ts
CHANGED
|
@@ -3,10 +3,11 @@ import { homedir } from "os";
|
|
|
3
3
|
import inquirer from "inquirer";
|
|
4
4
|
import { existsSync, readdirSync, readFileSync } from "fs";
|
|
5
5
|
import { loadPackageJson } from "./shared";
|
|
6
|
-
import fetch from "node-fetch";
|
|
7
6
|
import cuid from "cuid";
|
|
8
7
|
import { b, green, red } from "../lib/chalk-code-highlight";
|
|
9
8
|
import { getFunctionFromFilePath } from "../lib/compiled-function";
|
|
9
|
+
import { loadProjectConfig } from "../lib/project-config";
|
|
10
|
+
import { readDefaultWorkspace } from "../lib/auth-file";
|
|
10
11
|
|
|
11
12
|
function readLoginFile() {
|
|
12
13
|
const configFile = `${homedir()}/.jitsu/jitsu-cli.json`;
|
|
@@ -56,34 +57,57 @@ export async function deploy({ dir, workspace, name: names, ...params }: Args) {
|
|
|
56
57
|
}
|
|
57
58
|
const workspaces = (await res.json()) as any[];
|
|
58
59
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
60
|
+
// Which workspace to deploy to. Precedence:
|
|
61
|
+
// -w/--workspace flag → project config (jitsu.json / package.json "jitsu")
|
|
62
|
+
// → default workspace in ~/.jitsu/jitsu-cli.json → interactive prompt.
|
|
63
|
+
// The reference may be an id or a slug — both are matched against the list.
|
|
64
|
+
const findWorkspace = (ref?: string) => (ref ? workspaces.find(w => w.id === ref || w.slug === ref) : undefined);
|
|
65
|
+
const projectConfig = loadProjectConfig(projectDir, packageJson);
|
|
66
|
+
|
|
67
|
+
let workspaceObj: Workspace | undefined;
|
|
68
|
+
if (workspace) {
|
|
69
|
+
// Explicit -w/--workspace expresses clear intent: fail hard if it doesn't resolve.
|
|
70
|
+
workspaceObj = findWorkspace(workspace);
|
|
71
|
+
if (!workspaceObj) {
|
|
72
|
+
console.error(red(`Workspace '${b(workspace)}' not found (or you don't have access)`));
|
|
63
73
|
process.exit(1);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
74
|
+
}
|
|
75
|
+
} else {
|
|
76
|
+
// "Soft" default from project config or ~/.jitsu. If it doesn't resolve (stale,
|
|
77
|
+
// deleted, wrong account), fall back to auto-select / interactive prompt instead
|
|
78
|
+
// of hard-failing — preserving the pre-config behavior of `deploy` with no -w.
|
|
79
|
+
const softRef = projectConfig.workspace ?? readDefaultWorkspace();
|
|
80
|
+
workspaceObj = findWorkspace(softRef);
|
|
81
|
+
if (!workspaceObj) {
|
|
82
|
+
if (softRef) {
|
|
83
|
+
console.warn(`Configured workspace ${b(softRef)} not found or not accessible. Selecting manually.`);
|
|
84
|
+
}
|
|
85
|
+
if (workspaces.length === 0) {
|
|
86
|
+
console.error(`${red("No workspaces found")}`);
|
|
87
|
+
process.exit(1);
|
|
88
|
+
} else if (workspaces.length === 1) {
|
|
89
|
+
workspaceObj = workspaces[0];
|
|
90
|
+
} else {
|
|
91
|
+
const workspaceId = (
|
|
92
|
+
await inquirer.prompt([
|
|
93
|
+
{
|
|
94
|
+
type: "list",
|
|
95
|
+
name: "workspaceId",
|
|
96
|
+
message: `Select workspace:`,
|
|
97
|
+
choices: workspaces.map(w => ({
|
|
98
|
+
name: `${w.name} (${w.id})`,
|
|
99
|
+
value: w.id,
|
|
100
|
+
})),
|
|
101
|
+
},
|
|
102
|
+
])
|
|
103
|
+
).workspaceId;
|
|
104
|
+
workspaceObj = findWorkspace(workspaceId);
|
|
105
|
+
}
|
|
80
106
|
}
|
|
81
107
|
}
|
|
82
108
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
if (!workspaceId || !workspaceName) {
|
|
86
|
-
console.error(red(`Workspace with id ${workspaceId} not found`));
|
|
109
|
+
if (!workspaceObj?.id || !workspaceObj?.name) {
|
|
110
|
+
console.error(red(`Workspace not found`));
|
|
87
111
|
process.exit(1);
|
|
88
112
|
}
|
|
89
113
|
await deployFunctions({ ...params, host, apikey, name: names }, projectDir, packageJson, workspaceObj, "function");
|
|
@@ -101,12 +125,14 @@ async function deployFunctions(
|
|
|
101
125
|
const dir = `dist/${kind}s`;
|
|
102
126
|
const functionsDir = path.resolve(projectDir, dir);
|
|
103
127
|
|
|
128
|
+
if (!existsSync(functionsDir)) {
|
|
129
|
+
console.warn(`No ${b(dir)} directory found, skipping ${kind}s. Please make sure that you have built the project.`);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
104
132
|
const functionsFiles = readdirSync(functionsDir);
|
|
105
133
|
if (functionsFiles.length === 0) {
|
|
106
|
-
console.warn(
|
|
107
|
-
|
|
108
|
-
);
|
|
109
|
-
process.exit(1);
|
|
134
|
+
console.warn(`No ${kind} files found in ${b(dir)}, skipping. Please make sure that you have built the project.`);
|
|
135
|
+
return;
|
|
110
136
|
}
|
|
111
137
|
const selectedFiles: string[] = [];
|
|
112
138
|
if (selected) {
|
|
@@ -144,6 +170,11 @@ async function deployFunctions(
|
|
|
144
170
|
profileBuilders = ((await res.json()) as any).profileBuilders as any[];
|
|
145
171
|
}
|
|
146
172
|
|
|
173
|
+
// Fetch the existing function list once for slug/id resolution. Previously
|
|
174
|
+
// every deployFunction() call refetched the whole list (with code blobs) —
|
|
175
|
+
// O(N) requests per deploy, each pulling all N rows from the console DB.
|
|
176
|
+
const existingFunctions = await fetchExistingFunctions({ host, apikey, workspaceId: workspace.id! });
|
|
177
|
+
|
|
147
178
|
for (const file of selectedFiles) {
|
|
148
179
|
console.log(
|
|
149
180
|
`${b(`𝑓`)} Deploying function ${b(path.basename(file))} to workspace ${workspace.name} (${host}/${
|
|
@@ -151,27 +182,98 @@ async function deployFunctions(
|
|
|
151
182
|
})`
|
|
152
183
|
);
|
|
153
184
|
await deployFunction(
|
|
185
|
+
projectDir,
|
|
154
186
|
{ host, apikey },
|
|
155
187
|
packageJson,
|
|
156
188
|
workspace,
|
|
157
189
|
kind,
|
|
158
190
|
path.resolve(functionsDir, file),
|
|
159
|
-
profileBuilders
|
|
191
|
+
profileBuilders,
|
|
192
|
+
existingFunctions
|
|
160
193
|
);
|
|
161
194
|
}
|
|
162
195
|
}
|
|
163
196
|
|
|
197
|
+
// Cache of functions already present in the workspace. byId and bySlug share
|
|
198
|
+
// object identity so a single mutation visible from both. `slug` is tracked
|
|
199
|
+
// per entry so we can detect (and reflect) renames on PUT.
|
|
200
|
+
type ExistingFunction = { id: string; slug?: string };
|
|
201
|
+
type ExistingFunctionsCache = {
|
|
202
|
+
bySlug: Map<string, ExistingFunction>;
|
|
203
|
+
byId: Map<string, ExistingFunction>;
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
async function fetchExistingFunctions({
|
|
207
|
+
host,
|
|
208
|
+
apikey,
|
|
209
|
+
workspaceId,
|
|
210
|
+
}: {
|
|
211
|
+
host?: string;
|
|
212
|
+
apikey?: string;
|
|
213
|
+
workspaceId?: string;
|
|
214
|
+
}): Promise<ExistingFunctionsCache> {
|
|
215
|
+
const res = await fetch(`${host}/api/${workspaceId}/config/function`, {
|
|
216
|
+
headers: { Authorization: `Bearer ${apikey}` },
|
|
217
|
+
});
|
|
218
|
+
if (!res.ok) {
|
|
219
|
+
console.error(red(`Cannot list existing functions:\n${b(await res.text())}`));
|
|
220
|
+
process.exit(1);
|
|
221
|
+
}
|
|
222
|
+
const { objects } = (await res.json()) as { objects: { id: string; slug?: string }[] };
|
|
223
|
+
const bySlug = new Map<string, ExistingFunction>();
|
|
224
|
+
const byId = new Map<string, ExistingFunction>();
|
|
225
|
+
for (const f of objects) {
|
|
226
|
+
const entry: ExistingFunction = { id: f.id, slug: f.slug };
|
|
227
|
+
byId.set(f.id, entry);
|
|
228
|
+
if (f.slug) bySlug.set(f.slug, entry);
|
|
229
|
+
}
|
|
230
|
+
return { bySlug, byId };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Update the cache after a successful POST. Both maps must reflect the new
|
|
234
|
+
// function so a later file with the same slug/id in this same deploy run
|
|
235
|
+
// switches to PUT instead of trying a second POST.
|
|
236
|
+
function cacheAfterCreate(cache: ExistingFunctionsCache, id: string, slug: string | undefined) {
|
|
237
|
+
const entry: ExistingFunction = { id, slug };
|
|
238
|
+
cache.byId.set(id, entry);
|
|
239
|
+
if (slug) cache.bySlug.set(slug, entry);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Update the cache after a successful PUT. Preserves the previous re-fetch
|
|
243
|
+
// semantics for slug renames: the old slug is no longer pointing at this id
|
|
244
|
+
// on the server, so drop it from the slug index and install the new one.
|
|
245
|
+
function cacheAfterUpdate(cache: ExistingFunctionsCache, id: string, newSlug: string | undefined) {
|
|
246
|
+
const entry = cache.byId.get(id);
|
|
247
|
+
if (!entry) {
|
|
248
|
+
// Function existed only on the server, not in our hoisted cache (shouldn't
|
|
249
|
+
// normally happen since the PUT branch only runs when we resolved an id).
|
|
250
|
+
// Insert defensively so later files in this run see it.
|
|
251
|
+
cacheAfterCreate(cache, id, newSlug);
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
if (entry.slug && entry.slug !== newSlug) {
|
|
255
|
+
cache.bySlug.delete(entry.slug);
|
|
256
|
+
}
|
|
257
|
+
entry.slug = newSlug;
|
|
258
|
+
if (newSlug) cache.bySlug.set(newSlug, entry);
|
|
259
|
+
}
|
|
260
|
+
|
|
164
261
|
async function deployFunction(
|
|
262
|
+
projectDir: string,
|
|
165
263
|
{ host, apikey }: Args,
|
|
166
264
|
packageJson: any,
|
|
167
265
|
workspace: Workspace,
|
|
168
266
|
kind: "function" | "profile",
|
|
169
267
|
file: string,
|
|
170
|
-
profileBuilders: any[] = []
|
|
268
|
+
profileBuilders: any[] = [],
|
|
269
|
+
existingFunctions: ExistingFunctionsCache = {
|
|
270
|
+
bySlug: new Map(),
|
|
271
|
+
byId: new Map(),
|
|
272
|
+
}
|
|
171
273
|
) {
|
|
172
274
|
const code = readFileSync(file, "utf-8");
|
|
173
275
|
|
|
174
|
-
const wrapped = await getFunctionFromFilePath(file, kind, profileBuilders);
|
|
276
|
+
const wrapped = await getFunctionFromFilePath(projectDir, file, kind, profileBuilders);
|
|
175
277
|
const meta = wrapped.meta;
|
|
176
278
|
if (meta) {
|
|
177
279
|
console.log(` meta: slug=${meta.slug}, name=${meta.name || "not set"}`);
|
|
@@ -181,18 +283,8 @@ async function deployFunction(
|
|
|
181
283
|
}
|
|
182
284
|
let existingFunctionId: string | undefined;
|
|
183
285
|
if (meta.slug) {
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
Authorization: `Bearer ${apikey}`,
|
|
187
|
-
},
|
|
188
|
-
});
|
|
189
|
-
if (!res.ok) {
|
|
190
|
-
console.error(red(`Cannot add function to workspace:\n${b(await res.text())}`));
|
|
191
|
-
process.exit(1);
|
|
192
|
-
} else {
|
|
193
|
-
const existing = (await res.json()) as any;
|
|
194
|
-
existingFunctionId = existing.objects.find(f => f.slug === meta.slug || f.id === meta.id)?.id;
|
|
195
|
-
}
|
|
286
|
+
existingFunctionId =
|
|
287
|
+
existingFunctions.bySlug.get(meta.slug)?.id ?? (meta.id ? existingFunctions.byId.get(meta.id)?.id : undefined);
|
|
196
288
|
}
|
|
197
289
|
let functionPayload = {};
|
|
198
290
|
if (kind === "profile") {
|
|
@@ -230,6 +322,11 @@ async function deployFunction(
|
|
|
230
322
|
console.error(red(`Cannot add function to workspace:\n${b(await res.text())}`));
|
|
231
323
|
process.exit(1);
|
|
232
324
|
} else {
|
|
325
|
+
// Reflect the new function in the hoisted cache so a later file in
|
|
326
|
+
// this same deploy that targets the same slug switches to PUT instead
|
|
327
|
+
// of POSTing a duplicate. Matches the pre-hoist behavior where each
|
|
328
|
+
// file re-fetched the list.
|
|
329
|
+
cacheAfterCreate(existingFunctions, id, meta.slug);
|
|
233
330
|
console.log(`Function ${b(meta.name)} was successfully added to workspace ${workspace.name} with id: ${b(id)}`);
|
|
234
331
|
}
|
|
235
332
|
} else {
|
|
@@ -255,6 +352,10 @@ async function deployFunction(
|
|
|
255
352
|
console.error(red(`⚠ Cannot deploy function ${b(meta.slug)}(${id}):\n${b(await res.text())}`));
|
|
256
353
|
process.exit(1);
|
|
257
354
|
} else {
|
|
355
|
+
// Slug may have been renamed by this PUT — update the slug index so a
|
|
356
|
+
// later file deploying under the old slug (if any) creates a new
|
|
357
|
+
// function instead of clobbering this one.
|
|
358
|
+
cacheAfterUpdate(existingFunctions, id, meta.slug);
|
|
258
359
|
console.log(`${green(`✓`)} ${b(meta.name)} deployed successfully!`);
|
|
259
360
|
}
|
|
260
361
|
}
|
package/src/commands/login.ts
CHANGED
|
@@ -35,7 +35,9 @@ export async function logout({ force }: { force?: boolean }) {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
export async function login({ host, apikey, force }: { host: string; apikey?: string; force?: boolean }) {
|
|
38
|
-
const
|
|
38
|
+
const jitsuDir = `${homedir()}/.jitsu`;
|
|
39
|
+
fs.mkdirSync(jitsuDir, { recursive: true });
|
|
40
|
+
const jitsuFile = `${jitsuDir}/jitsu-cli.json`;
|
|
39
41
|
if (fs.existsSync(jitsuFile) && !force) {
|
|
40
42
|
const loginInfo = JSON.parse(readFileSync(jitsuFile, { encoding: "utf-8" }));
|
|
41
43
|
console.error(
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { red } from "../lib/chalk-code-highlight";
|
|
3
|
+
import { normalizeHost, readAuthFile, resolveAuth } from "../lib/auth-file";
|
|
4
|
+
import { fetchSpec } from "../lib/spec";
|
|
5
|
+
import { DEFAULT_OUTPUT, SUPPORTED_OUTPUTS, print } from "../lib/renderer";
|
|
6
|
+
|
|
7
|
+
export function buildSpecCommand(): Command {
|
|
8
|
+
return new Command("spec")
|
|
9
|
+
.description("Print the live OpenAPI spec served at /api/spec")
|
|
10
|
+
.option("-o, --output <format>", `Output format: ${SUPPORTED_OUTPUTS.join(", ")}`, DEFAULT_OUTPUT)
|
|
11
|
+
.option("-h, --host <host>", "Jitsu host (overrides ~/.jitsu/jitsu-cli.json)")
|
|
12
|
+
.option("-k, --apikey <api-key>", "API key (overrides ~/.jitsu/jitsu-cli.json)")
|
|
13
|
+
.action(async (opts: { output?: string; host?: string; apikey?: string }) => {
|
|
14
|
+
try {
|
|
15
|
+
// Pass a placeholder apikey if missing — /api/spec is public, but resolveAuth
|
|
16
|
+
// expects an apikey. Fall back to a synthetic one if user has no auth file.
|
|
17
|
+
const auth = (() => {
|
|
18
|
+
try {
|
|
19
|
+
return resolveAuth(opts);
|
|
20
|
+
} catch {
|
|
21
|
+
const host = opts.host ?? readAuthFile()?.host ?? "https://use.jitsu.com";
|
|
22
|
+
return { host: normalizeHost(host), apikey: "anonymous" };
|
|
23
|
+
}
|
|
24
|
+
})();
|
|
25
|
+
const spec = await fetchSpec(auth);
|
|
26
|
+
print(spec, opts.output);
|
|
27
|
+
} catch (e) {
|
|
28
|
+
console.error(red(`Error: ${e instanceof Error ? e.message : String(e)}`));
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -8,12 +8,27 @@ import { test } from "./commands/test";
|
|
|
8
8
|
|
|
9
9
|
import { jitsuCliVersion, jitsuCliPackageName } from "./lib/version";
|
|
10
10
|
import { whoami } from "./commands/whoami";
|
|
11
|
+
import { buildConfigCommand } from "./commands/config";
|
|
12
|
+
import { buildSpecCommand } from "./commands/spec";
|
|
13
|
+
import { setDefaultWorkspace, unsetDefaultWorkspace } from "./commands/default-workspace";
|
|
14
|
+
import { preprocessArgv } from "./lib/body-fields";
|
|
11
15
|
|
|
12
|
-
|
|
16
|
+
// Pull ad-hoc body field flags (--name=val, --credentials.password=...) out of argv
|
|
17
|
+
// before Commander parses it. Reserved option names are left in place.
|
|
18
|
+
process.argv = preprocessArgv(process.argv);
|
|
19
|
+
|
|
20
|
+
// Figlet banner is decorative — write it to stderr so `jitsu config ... -o json | jq`
|
|
21
|
+
// works without filtering. Skip it entirely when stdout is being piped or for spec output
|
|
22
|
+
// (which is also typically piped to other tools).
|
|
23
|
+
const isPipedStdout = !process.stdout.isTTY;
|
|
24
|
+
if (!isPipedStdout) {
|
|
25
|
+
process.stderr.write(figlet.textSync("Jitsu CLI", { horizontalLayout: "full" }) + "\n");
|
|
26
|
+
}
|
|
13
27
|
|
|
14
28
|
const p = new Command();
|
|
15
29
|
|
|
16
|
-
p.name(jitsuCliPackageName).description("CLI
|
|
30
|
+
p.name(jitsuCliPackageName).description("Jitsu CLI — manage workspaces, configuration objects, and extensions");
|
|
31
|
+
|
|
17
32
|
p.command("init")
|
|
18
33
|
.description("Initialize a new Jitsu extension project")
|
|
19
34
|
.arguments("[dir]")
|
|
@@ -31,19 +46,6 @@ p.command("test")
|
|
|
31
46
|
.option("-d, --dir <dir>", "the directory of project. (Optional). By default, current directory is used")
|
|
32
47
|
.action(test);
|
|
33
48
|
|
|
34
|
-
// p.command("run")
|
|
35
|
-
// .description("Check extensions on provided event, config and persistent storage state")
|
|
36
|
-
// .option("-d, --dir <dir>", "the directory of project. (Optional). By default, current directory is used")
|
|
37
|
-
// .option(
|
|
38
|
-
// "-n, --name <name>",
|
|
39
|
-
// "name of function file to check (optional). Required if multiple functions are defined in project"
|
|
40
|
-
// )
|
|
41
|
-
// .option("-t, --type <type>", "entity type to run", "function")
|
|
42
|
-
// .requiredOption("-e, --event <file_or_json>", "path to file with event json or event json as a string")
|
|
43
|
-
// .option("-p, --props <file_or_json>", "path to file with config json or config json as a string. (Optional)")
|
|
44
|
-
// .option("-s, --store <file_or_json>", "path to file with state json or state json as a string. (Optional)")
|
|
45
|
-
// .action(run);
|
|
46
|
-
|
|
47
49
|
p.command("whoami")
|
|
48
50
|
.description("Check if current user is logged in. Shows user's info if logged in")
|
|
49
51
|
.option("-h, --host <host>", "Jitsu host or base url", "https://use.jitsu.com")
|
|
@@ -56,6 +58,7 @@ p.command("login")
|
|
|
56
58
|
.option("-h, --host <host>", "Jitsu host or base url", "https://use.jitsu.com")
|
|
57
59
|
.option("-k, --apikey <api-key>", "Jitsu user's Api Key. (Optional). Disables interactive login.")
|
|
58
60
|
.action(login);
|
|
61
|
+
|
|
59
62
|
p.command("logout").description("Logout").option("-f, --force", "Do not ask for confirmation").action(logout);
|
|
60
63
|
|
|
61
64
|
p.command("deploy")
|
|
@@ -68,16 +71,30 @@ p.command("deploy")
|
|
|
68
71
|
)
|
|
69
72
|
.option("-k, --apikey <api-key>", "(Optional) Jitsu user's Api Key.")
|
|
70
73
|
.option(
|
|
71
|
-
"-w, --workspace <
|
|
72
|
-
|
|
74
|
+
"-w, --workspace <id-or-slug>",
|
|
75
|
+
'Workspace id or slug to deploy to (Optional). Falls back to jitsu.json / package.json "jitsu" config, then the default workspace, then an interactive prompt'
|
|
73
76
|
)
|
|
74
77
|
.option("-t, --type <type>", "entity type to deploy", "function")
|
|
75
78
|
.option("-n, --name <name...>", "limit deploy to provided entities only. (Optional)")
|
|
76
79
|
.action(deploy);
|
|
77
80
|
|
|
78
|
-
|
|
81
|
+
p.command("set-default-workspace")
|
|
82
|
+
.description(
|
|
83
|
+
"Save a default workspace to ~/.jitsu/jitsu-cli.json. Subsequent `jitsu config` commands use it when -w is omitted."
|
|
84
|
+
)
|
|
85
|
+
.argument("<id-or-slug>", "Workspace id or slug")
|
|
86
|
+
.option("-h, --host <host>", "Jitsu host (overrides ~/.jitsu/jitsu-cli.json)")
|
|
87
|
+
.option("-k, --apikey <api-key>", "API key in form keyId:secret (overrides ~/.jitsu/jitsu-cli.json)")
|
|
88
|
+
.action(setDefaultWorkspace);
|
|
89
|
+
|
|
90
|
+
p.command("unset-default-workspace")
|
|
91
|
+
.description("Remove the saved default workspace from ~/.jitsu/jitsu-cli.json")
|
|
92
|
+
.action(unsetDefaultWorkspace);
|
|
93
|
+
|
|
94
|
+
p.addCommand(buildConfigCommand());
|
|
95
|
+
p.addCommand(buildSpecCommand());
|
|
96
|
+
|
|
79
97
|
p.version(jitsuCliPackageName + " " + jitsuCliVersion, "-v, --version");
|
|
80
|
-
//help
|
|
81
98
|
p.helpOption("--help", "display help for command");
|
|
82
99
|
|
|
83
100
|
p.parse();
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { AuthInfo } from "./auth-file";
|
|
2
|
+
|
|
3
|
+
export type ApiRequest = {
|
|
4
|
+
method?: "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
|
|
5
|
+
path: string;
|
|
6
|
+
query?: Record<string, string | number | boolean | undefined | null>;
|
|
7
|
+
body?: unknown;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export class ApiError extends Error {
|
|
11
|
+
status: number;
|
|
12
|
+
payload: unknown;
|
|
13
|
+
constructor(status: number, message: string, payload: unknown) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.status = status;
|
|
16
|
+
this.payload = payload;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class ApiClient {
|
|
21
|
+
constructor(private auth: AuthInfo) {}
|
|
22
|
+
|
|
23
|
+
url(path: string, query?: ApiRequest["query"]): string {
|
|
24
|
+
const base = this.auth.host;
|
|
25
|
+
const p = path.startsWith("/") ? path : `/${path}`;
|
|
26
|
+
if (!query) return `${base}${p}`;
|
|
27
|
+
const qs = Object.entries(query)
|
|
28
|
+
.filter(([, v]) => v !== undefined && v !== null && v !== "")
|
|
29
|
+
.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`)
|
|
30
|
+
.join("&");
|
|
31
|
+
return qs ? `${base}${p}?${qs}` : `${base}${p}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async request<T = unknown>(req: ApiRequest): Promise<T> {
|
|
35
|
+
const method = req.method ?? "GET";
|
|
36
|
+
const headers: Record<string, string> = {
|
|
37
|
+
Accept: "application/json",
|
|
38
|
+
Authorization: `Bearer ${this.auth.apikey}`,
|
|
39
|
+
};
|
|
40
|
+
let body: string | undefined;
|
|
41
|
+
if (req.body !== undefined) {
|
|
42
|
+
headers["Content-Type"] = "application/json";
|
|
43
|
+
body = JSON.stringify(req.body);
|
|
44
|
+
}
|
|
45
|
+
const res = await fetch(this.url(req.path, req.query), { method, headers, body });
|
|
46
|
+
const text = await res.text();
|
|
47
|
+
let parsed: unknown = text;
|
|
48
|
+
if (text && res.headers.get("content-type")?.includes("application/json")) {
|
|
49
|
+
try {
|
|
50
|
+
parsed = JSON.parse(text);
|
|
51
|
+
} catch {
|
|
52
|
+
// fall through with raw text
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (!res.ok) {
|
|
56
|
+
const msg =
|
|
57
|
+
(parsed && typeof parsed === "object" && "message" in (parsed as any)
|
|
58
|
+
? String((parsed as any).message)
|
|
59
|
+
: undefined) ?? `HTTP ${res.status} ${method} ${req.path}`;
|
|
60
|
+
throw new ApiError(res.status, msg, parsed);
|
|
61
|
+
}
|
|
62
|
+
return parsed as T;
|
|
63
|
+
}
|
|
64
|
+
}
|