@baseworks/organization 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-5UCSEIJS.js +64 -0
- package/dist/cli.d.ts +18 -0
- package/dist/cli.js +534 -0
- package/dist/index.d.ts +199 -0
- package/dist/index.js +335 -0
- package/dist/schema/pg/index.d.ts +562 -0
- package/dist/schema/pg/index.js +62 -0
- package/dist/schema/sqlite/index.d.ts +604 -0
- package/dist/schema/sqlite/index.js +12 -0
- package/package.json +37 -0
- package/src/__tests__/cli-env.test.ts +158 -0
- package/src/__tests__/cli-org.test.ts +154 -0
- package/src/__tests__/cli-proj.test.ts +157 -0
- package/src/__tests__/cli-ws.test.ts +156 -0
- package/src/__tests__/helpers.ts +29 -0
- package/src/cli.ts +682 -0
- package/src/index.ts +5 -0
- package/src/operations/bootstrap.ts +50 -0
- package/src/repo/environments.ts +82 -0
- package/src/repo/index.ts +9 -0
- package/src/repo/organizations.ts +96 -0
- package/src/repo/projects.ts +106 -0
- package/src/repo/workspaces.ts +87 -0
- package/src/schema/environments.ts +14 -0
- package/src/schema/index.ts +5 -0
- package/src/schema/organizations.ts +11 -0
- package/src/schema/pg/environments.ts +14 -0
- package/src/schema/pg/index.ts +4 -0
- package/src/schema/pg/organizations.ts +11 -0
- package/src/schema/pg/projects.ts +16 -0
- package/src/schema/pg/workspaces.ts +15 -0
- package/src/schema/projects.ts +16 -0
- package/src/schema/sqlite/environments.ts +14 -0
- package/src/schema/sqlite/index.ts +4 -0
- package/src/schema/sqlite/organizations.ts +11 -0
- package/src/schema/sqlite/projects.ts +16 -0
- package/src/schema/sqlite/workspaces.ts +15 -0
- package/src/schema/workspaces.ts +15 -0
- package/src/types.ts +88 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// src/schema/sqlite/organizations.ts
|
|
2
|
+
import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
|
|
3
|
+
var organizations = sqliteTable("organizations", {
|
|
4
|
+
id: text("id").primaryKey(),
|
|
5
|
+
shortId: text("short_id").notNull().unique(),
|
|
6
|
+
slug: text("slug").notNull().unique(),
|
|
7
|
+
name: text("name").notNull(),
|
|
8
|
+
metadata: text("metadata"),
|
|
9
|
+
// JSON: { logo_url, website, ... }
|
|
10
|
+
createdAt: integer("created_at").notNull(),
|
|
11
|
+
updatedAt: integer("updated_at").notNull()
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
// src/schema/sqlite/workspaces.ts
|
|
15
|
+
import { integer as integer2, sqliteTable as sqliteTable2, text as text2, uniqueIndex } from "drizzle-orm/sqlite-core";
|
|
16
|
+
var workspaces = sqliteTable2("workspaces", {
|
|
17
|
+
id: text2("id").primaryKey(),
|
|
18
|
+
organizationId: text2("organization_id").notNull().references(() => organizations.id, { onDelete: "cascade" }),
|
|
19
|
+
shortId: text2("short_id").notNull().unique(),
|
|
20
|
+
slug: text2("slug").notNull(),
|
|
21
|
+
name: text2("name").notNull(),
|
|
22
|
+
isDefault: integer2("is_default", { mode: "boolean" }).notNull().default(false),
|
|
23
|
+
createdAt: integer2("created_at").notNull(),
|
|
24
|
+
updatedAt: integer2("updated_at").notNull()
|
|
25
|
+
}, (t) => ({
|
|
26
|
+
orgSlugUniq: uniqueIndex("workspaces_org_slug_uniq").on(t.organizationId, t.slug)
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
// src/schema/sqlite/projects.ts
|
|
30
|
+
import { integer as integer3, sqliteTable as sqliteTable3, text as text3, uniqueIndex as uniqueIndex2 } from "drizzle-orm/sqlite-core";
|
|
31
|
+
var projects = sqliteTable3("projects", {
|
|
32
|
+
id: text3("id").primaryKey(),
|
|
33
|
+
workspaceId: text3("workspace_id").notNull().references(() => workspaces.id, { onDelete: "cascade" }),
|
|
34
|
+
shortId: text3("short_id").notNull().unique(),
|
|
35
|
+
slug: text3("slug").notNull(),
|
|
36
|
+
name: text3("name").notNull(),
|
|
37
|
+
isDefault: integer3("is_default", { mode: "boolean" }).notNull().default(false),
|
|
38
|
+
metadata: text3("metadata"),
|
|
39
|
+
createdAt: integer3("created_at").notNull(),
|
|
40
|
+
updatedAt: integer3("updated_at").notNull()
|
|
41
|
+
}, (t) => ({
|
|
42
|
+
workspaceSlugUniq: uniqueIndex2("projects_workspace_slug_uniq").on(t.workspaceId, t.slug)
|
|
43
|
+
}));
|
|
44
|
+
|
|
45
|
+
// src/schema/sqlite/environments.ts
|
|
46
|
+
import { integer as integer4, sqliteTable as sqliteTable4, text as text4, uniqueIndex as uniqueIndex3 } from "drizzle-orm/sqlite-core";
|
|
47
|
+
var environments = sqliteTable4("environments", {
|
|
48
|
+
id: text4("id").primaryKey(),
|
|
49
|
+
projectId: text4("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
|
|
50
|
+
shortId: text4("short_id").notNull().unique(),
|
|
51
|
+
slug: text4("slug").notNull(),
|
|
52
|
+
name: text4("name").notNull(),
|
|
53
|
+
createdAt: integer4("created_at").notNull(),
|
|
54
|
+
updatedAt: integer4("updated_at").notNull()
|
|
55
|
+
}, (t) => ({
|
|
56
|
+
projectSlugUniq: uniqueIndex3("environments_project_slug_uniq").on(t.projectId, t.slug)
|
|
57
|
+
}));
|
|
58
|
+
|
|
59
|
+
export {
|
|
60
|
+
organizations,
|
|
61
|
+
workspaces,
|
|
62
|
+
projects,
|
|
63
|
+
environments
|
|
64
|
+
};
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { ApiClient } from '@baseworks/cli/client';
|
|
3
|
+
import { ContextManager } from '@baseworks/cli/context';
|
|
4
|
+
|
|
5
|
+
interface OrgCliDeps {
|
|
6
|
+
http: ApiClient;
|
|
7
|
+
ctx: ContextManager<Record<string, string | undefined>>;
|
|
8
|
+
appBase: string;
|
|
9
|
+
cliName?: string;
|
|
10
|
+
}
|
|
11
|
+
declare function buildOrgCommand(deps: OrgCliDeps): Command;
|
|
12
|
+
declare function buildWsCommand(deps: OrgCliDeps): Command;
|
|
13
|
+
declare function buildProjCommand(deps: OrgCliDeps): Command;
|
|
14
|
+
declare function buildEnvCommand(deps: OrgCliDeps): Command;
|
|
15
|
+
declare function addGlobalCommands(program: Command, deps: OrgCliDeps): void;
|
|
16
|
+
declare function orgCommand(deps: OrgCliDeps): Command;
|
|
17
|
+
|
|
18
|
+
export { type OrgCliDeps, addGlobalCommands, buildEnvCommand, buildOrgCommand, buildProjCommand, buildWsCommand, orgCommand };
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,534 @@
|
|
|
1
|
+
// src/cli.ts
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import { clr, kv, table, summary, success, warn, fatal, printOutput, outputOption, prompt } from "@baseworks/cli/display";
|
|
4
|
+
import { fmtDate } from "@baseworks/cli/fmt";
|
|
5
|
+
import { tree } from "@baseworks/cli/tree";
|
|
6
|
+
function activeOrgSlug(ctx, cliName, flagOrg) {
|
|
7
|
+
return flagOrg ?? ctx.getContext()?.["org"] ?? ctx.loadConfig()["activeOrg"] ?? fatal(`No active org. Run: ${cliName} use <org>`);
|
|
8
|
+
}
|
|
9
|
+
function activeWsSlug(ctx, cliName, flagWs) {
|
|
10
|
+
return flagWs ?? ctx.getContext()?.["workspace"] ?? fatal(`No active workspace. Run: ${cliName} use <org>/<ws>`);
|
|
11
|
+
}
|
|
12
|
+
function activeProjSlug(ctx, cliName, flagProj) {
|
|
13
|
+
return flagProj ?? ctx.getContext()?.["project"] ?? fatal(`No active project. Run: ${cliName} use <org>/<ws>/<proj>`);
|
|
14
|
+
}
|
|
15
|
+
function buildOrgCommand(deps) {
|
|
16
|
+
const { http } = deps;
|
|
17
|
+
const cli = deps.cliName ?? "cli";
|
|
18
|
+
const cmd = new Command("org").description("Organization management").option(...outputOption()).action(async (opts) => {
|
|
19
|
+
const res = await http.get("/v1/orgs").catch((e) => {
|
|
20
|
+
console.error(e.message);
|
|
21
|
+
process.exit(1);
|
|
22
|
+
});
|
|
23
|
+
printOutput(res.orgs, opts.output, (wide) => {
|
|
24
|
+
table(res.orgs, [
|
|
25
|
+
{ key: "shortId", label: "ID" },
|
|
26
|
+
{ key: "slug", label: "SLUG" },
|
|
27
|
+
{ key: "name", label: "NAME" },
|
|
28
|
+
{ key: "createdAt", label: "CREATED", wide: true, fmt: (v) => fmtDate(v) },
|
|
29
|
+
{ key: "id", label: "FULL ID", wide: true }
|
|
30
|
+
], { wide, emptyHint: `No orgs yet. Run: ${cli} org create --name <n>` });
|
|
31
|
+
summary(`${res.orgs.length} org${res.orgs.length !== 1 ? "s" : ""}`);
|
|
32
|
+
}, "slug");
|
|
33
|
+
});
|
|
34
|
+
cmd.addCommand(
|
|
35
|
+
new Command("create").description("Create a new organization").requiredOption("--name <name>", "Display name").option("--slug <slug>", "URL slug (auto-generated if omitted)").action(async (opts) => {
|
|
36
|
+
const res = await http.post("/v1/orgs", { name: opts.name, slug: opts.slug }).catch((e) => {
|
|
37
|
+
console.error(e.message);
|
|
38
|
+
process.exit(1);
|
|
39
|
+
});
|
|
40
|
+
success(`Org created: ${res.org.slug}`);
|
|
41
|
+
kv([["id", res.org.shortId], ["slug", res.org.slug]]);
|
|
42
|
+
})
|
|
43
|
+
);
|
|
44
|
+
cmd.addCommand(
|
|
45
|
+
new Command("get").argument("<slug>").description("Get org details").option(...outputOption()).action(async (slug, opts) => {
|
|
46
|
+
const res = await http.get(`/v1/orgs/${slug}`).catch((e) => {
|
|
47
|
+
console.error(e.message);
|
|
48
|
+
process.exit(1);
|
|
49
|
+
});
|
|
50
|
+
const meta2 = res.org.metadata ?? {};
|
|
51
|
+
printOutput(res.org, opts.output, () => {
|
|
52
|
+
kv([["id", res.org.shortId], ["slug", res.org.slug], ["name", res.org.name], ["full_id", res.org.id]]);
|
|
53
|
+
if (Object.keys(meta2).length) {
|
|
54
|
+
console.log(` ${clr.dim}\u2500\u2500 metadata \u2500\u2500${clr.reset}`);
|
|
55
|
+
kv(Object.entries(meta2).map(([k, v]) => [k, JSON.stringify(v)]));
|
|
56
|
+
}
|
|
57
|
+
}, "slug");
|
|
58
|
+
})
|
|
59
|
+
);
|
|
60
|
+
cmd.addCommand(
|
|
61
|
+
new Command("update").argument("<slug>").description("Update org name or slug").option("--name <name>", "New display name").option("--slug <new-slug>", "New URL slug").action(async (slug, opts) => {
|
|
62
|
+
if (!opts.name && !opts.slug) {
|
|
63
|
+
warn("Provide at least --name or --slug");
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const body = {};
|
|
67
|
+
if (opts.name) body["name"] = opts.name;
|
|
68
|
+
if (opts.slug) body["slug"] = opts.slug;
|
|
69
|
+
const res = await http.patch(`/v1/orgs/${slug}`, body).catch((e) => {
|
|
70
|
+
console.error(e.message);
|
|
71
|
+
process.exit(1);
|
|
72
|
+
});
|
|
73
|
+
success("Org updated.");
|
|
74
|
+
kv([["id", res.org.shortId], ["slug", res.org.slug], ["name", res.org.name]]);
|
|
75
|
+
})
|
|
76
|
+
);
|
|
77
|
+
cmd.addCommand(
|
|
78
|
+
new Command("delete").argument("<slug>").description("Delete an organization").action(async (slug) => {
|
|
79
|
+
await http.del(`/v1/orgs/${slug}`).catch((e) => {
|
|
80
|
+
console.error(e.message);
|
|
81
|
+
process.exit(1);
|
|
82
|
+
});
|
|
83
|
+
success(`Org deleted: ${slug}`);
|
|
84
|
+
})
|
|
85
|
+
);
|
|
86
|
+
const meta = new Command("meta").description("Manage org metadata");
|
|
87
|
+
meta.addCommand(
|
|
88
|
+
new Command("get").argument("<slug>").argument("[key]").description("Show metadata (all or one key)").action(async (slug, key) => {
|
|
89
|
+
const res = await http.get(`/v1/orgs/${slug}`).catch((e) => {
|
|
90
|
+
console.error(e.message);
|
|
91
|
+
process.exit(1);
|
|
92
|
+
});
|
|
93
|
+
const m = res.org.metadata ?? {};
|
|
94
|
+
if (key) {
|
|
95
|
+
const val = m[key];
|
|
96
|
+
if (val === void 0) warn(`Key '${key}' not found.`);
|
|
97
|
+
else console.log(` ${clr.dim}${key}${clr.reset} ${JSON.stringify(val)}`);
|
|
98
|
+
} else if (!Object.keys(m).length) {
|
|
99
|
+
console.log(` ${clr.dim}(no metadata)${clr.reset}`);
|
|
100
|
+
} else {
|
|
101
|
+
kv(Object.entries(m).map(([k, v]) => [k, JSON.stringify(v)]));
|
|
102
|
+
}
|
|
103
|
+
})
|
|
104
|
+
);
|
|
105
|
+
meta.addCommand(
|
|
106
|
+
new Command("set").argument("<slug>").argument("<key>").argument("<value>").description("Set a metadata key (JSON or string)").action(async (slug, key, value) => {
|
|
107
|
+
let parsed;
|
|
108
|
+
try {
|
|
109
|
+
parsed = JSON.parse(value);
|
|
110
|
+
} catch {
|
|
111
|
+
parsed = value;
|
|
112
|
+
}
|
|
113
|
+
await http.patch(`/v1/orgs/${slug}/metadata`, { [key]: parsed }).catch((e) => {
|
|
114
|
+
console.error(e.message);
|
|
115
|
+
process.exit(1);
|
|
116
|
+
});
|
|
117
|
+
success(`Metadata key '${key}' set.`);
|
|
118
|
+
})
|
|
119
|
+
);
|
|
120
|
+
meta.addCommand(
|
|
121
|
+
new Command("delete-key").argument("<slug>").argument("<key>").description("Remove a metadata key").action(async (slug, key) => {
|
|
122
|
+
await http.del(`/v1/orgs/${slug}/metadata/${key}`).catch((e) => {
|
|
123
|
+
console.error(e.message);
|
|
124
|
+
process.exit(1);
|
|
125
|
+
});
|
|
126
|
+
success(`Metadata key '${key}' deleted.`);
|
|
127
|
+
})
|
|
128
|
+
);
|
|
129
|
+
cmd.addCommand(meta);
|
|
130
|
+
return cmd;
|
|
131
|
+
}
|
|
132
|
+
function buildWsCommand(deps) {
|
|
133
|
+
const { http, ctx } = deps;
|
|
134
|
+
const cli = deps.cliName ?? "cli";
|
|
135
|
+
const cmd = new Command("ws").alias("workspace").description("Workspace management").enablePositionalOptions().option("--org <slug>", "Override active org").option(...outputOption()).action(async (opts) => {
|
|
136
|
+
const orgSlug = activeOrgSlug(ctx, cli, opts.org);
|
|
137
|
+
const res = await http.get(`/v1/orgs/${orgSlug}/workspaces`).catch((e) => {
|
|
138
|
+
console.error(e.message);
|
|
139
|
+
process.exit(1);
|
|
140
|
+
});
|
|
141
|
+
printOutput(res.workspaces, opts.output, (wide) => {
|
|
142
|
+
table(res.workspaces, [
|
|
143
|
+
{ key: "shortId", label: "ID" },
|
|
144
|
+
{ key: "slug", label: "SLUG" },
|
|
145
|
+
{ key: "name", label: "NAME" },
|
|
146
|
+
{ key: "isDefault", label: "DEFAULT", fmt: (v) => v ? "\u2713" : "" },
|
|
147
|
+
{ key: "id", label: "FULL ID", wide: true }
|
|
148
|
+
], { wide, emptyHint: `No workspaces yet. Run: ${cli} ws create --name <n>` });
|
|
149
|
+
summary(`${res.workspaces.length} workspace${res.workspaces.length !== 1 ? "s" : ""}`);
|
|
150
|
+
}, "slug");
|
|
151
|
+
});
|
|
152
|
+
cmd.addCommand(
|
|
153
|
+
new Command("create").description("Create a workspace").requiredOption("--name <name>", "Display name").option("--slug <slug>", "URL slug (auto-generated if omitted)").option("--org <slug>", "Override active org").action(async (opts) => {
|
|
154
|
+
const orgSlug = activeOrgSlug(ctx, cli, opts.org);
|
|
155
|
+
const res = await http.post(`/v1/orgs/${orgSlug}/workspaces`, { name: opts.name, slug: opts.slug }).catch((e) => {
|
|
156
|
+
console.error(e.message);
|
|
157
|
+
process.exit(1);
|
|
158
|
+
});
|
|
159
|
+
success(`Workspace created: ${res.workspace.slug}`);
|
|
160
|
+
kv([["id", res.workspace.shortId], ["slug", res.workspace.slug]]);
|
|
161
|
+
})
|
|
162
|
+
);
|
|
163
|
+
cmd.addCommand(
|
|
164
|
+
new Command("get").argument("<ref>").description("Get workspace details").option("--org <slug>", "Override active org").option(...outputOption()).action(async (ref, opts) => {
|
|
165
|
+
const orgSlug = activeOrgSlug(ctx, cli, opts.org);
|
|
166
|
+
const res = await http.get(`/v1/orgs/${orgSlug}/workspaces/${ref}`).catch((e) => {
|
|
167
|
+
console.error(e.message);
|
|
168
|
+
process.exit(1);
|
|
169
|
+
});
|
|
170
|
+
printOutput(res.workspace, opts.output, () => {
|
|
171
|
+
kv([["id", res.workspace.shortId], ["slug", res.workspace.slug], ["name", res.workspace.name], ["full_id", res.workspace.id]]);
|
|
172
|
+
}, "slug");
|
|
173
|
+
})
|
|
174
|
+
);
|
|
175
|
+
cmd.addCommand(
|
|
176
|
+
new Command("update").argument("<ref>").description("Update a workspace").option("--name <name>", "New display name").option("--slug <new-slug>", "New URL slug").option("--org <slug>", "Override active org").action(async (ref, opts) => {
|
|
177
|
+
if (!opts.name && !opts.slug) {
|
|
178
|
+
warn("Provide at least --name or --slug");
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
const orgSlug = activeOrgSlug(ctx, cli, opts.org);
|
|
182
|
+
const body = {};
|
|
183
|
+
if (opts.name) body["name"] = opts.name;
|
|
184
|
+
if (opts.slug) body["slug"] = opts.slug;
|
|
185
|
+
const res = await http.patch(`/v1/orgs/${orgSlug}/workspaces/${ref}`, body).catch((e) => {
|
|
186
|
+
console.error(e.message);
|
|
187
|
+
process.exit(1);
|
|
188
|
+
});
|
|
189
|
+
success("Workspace updated.");
|
|
190
|
+
kv([["id", res.workspace.shortId], ["slug", res.workspace.slug], ["name", res.workspace.name]]);
|
|
191
|
+
})
|
|
192
|
+
);
|
|
193
|
+
cmd.addCommand(
|
|
194
|
+
new Command("delete").argument("<ref>").description("Delete a workspace").option("--org <slug>", "Override active org").action(async (ref, opts) => {
|
|
195
|
+
const orgSlug = activeOrgSlug(ctx, cli, opts.org);
|
|
196
|
+
await http.del(`/v1/orgs/${orgSlug}/workspaces/${ref}`).catch((e) => {
|
|
197
|
+
console.error(e.message);
|
|
198
|
+
process.exit(1);
|
|
199
|
+
});
|
|
200
|
+
success(`Workspace deleted: ${ref}`);
|
|
201
|
+
})
|
|
202
|
+
);
|
|
203
|
+
return cmd;
|
|
204
|
+
}
|
|
205
|
+
function buildProjCommand(deps) {
|
|
206
|
+
const { http, ctx } = deps;
|
|
207
|
+
const cli = deps.cliName ?? "cli";
|
|
208
|
+
const cmd = new Command("proj").alias("project").description("Project management").enablePositionalOptions().option("--org <slug>", "Override active org").option("--ws <slug>", "Override active workspace").option(...outputOption()).action(async (opts) => {
|
|
209
|
+
const orgSlug = activeOrgSlug(ctx, cli, opts.org);
|
|
210
|
+
const wsSlug = activeWsSlug(ctx, cli, opts.ws);
|
|
211
|
+
const res = await http.get(`/v1/orgs/${orgSlug}/workspaces/${wsSlug}/projects`).catch((e) => {
|
|
212
|
+
console.error(e.message);
|
|
213
|
+
process.exit(1);
|
|
214
|
+
});
|
|
215
|
+
printOutput(res.projects, opts.output, (wide) => {
|
|
216
|
+
table(res.projects, [
|
|
217
|
+
{ key: "shortId", label: "ID" },
|
|
218
|
+
{ key: "slug", label: "SLUG" },
|
|
219
|
+
{ key: "name", label: "NAME" },
|
|
220
|
+
{ key: "isDefault", label: "DEFAULT", fmt: (v) => v ? "\u2713" : "" },
|
|
221
|
+
{ key: "id", label: "FULL ID", wide: true }
|
|
222
|
+
], { wide, emptyHint: `No projects yet. Run: ${cli} proj create --name <n>` });
|
|
223
|
+
summary(`${res.projects.length} project${res.projects.length !== 1 ? "s" : ""}`);
|
|
224
|
+
}, "slug");
|
|
225
|
+
});
|
|
226
|
+
cmd.addCommand(
|
|
227
|
+
new Command("create").description("Create a project").requiredOption("--name <name>", "Display name").option("--slug <slug>", "URL slug (auto-generated if omitted)").option("--org <slug>", "Override active org").option("--ws <slug>", "Override active workspace").action(async (opts) => {
|
|
228
|
+
const orgSlug = activeOrgSlug(ctx, cli, opts.org);
|
|
229
|
+
const wsSlug = activeWsSlug(ctx, cli, opts.ws);
|
|
230
|
+
const res = await http.post(`/v1/orgs/${orgSlug}/workspaces/${wsSlug}/projects`, { name: opts.name, slug: opts.slug }).catch((e) => {
|
|
231
|
+
console.error(e.message);
|
|
232
|
+
process.exit(1);
|
|
233
|
+
});
|
|
234
|
+
success(`Project created: ${res.project.slug}`);
|
|
235
|
+
kv([["id", res.project.shortId], ["slug", res.project.slug]]);
|
|
236
|
+
})
|
|
237
|
+
);
|
|
238
|
+
cmd.addCommand(
|
|
239
|
+
new Command("get").argument("<ref>").description("Get project details").option("--org <slug>", "Override active org").option("--ws <slug>", "Override active workspace").option(...outputOption()).action(async (ref, opts) => {
|
|
240
|
+
const orgSlug = activeOrgSlug(ctx, cli, opts.org);
|
|
241
|
+
const wsSlug = activeWsSlug(ctx, cli, opts.ws);
|
|
242
|
+
const res = await http.get(`/v1/orgs/${orgSlug}/workspaces/${wsSlug}/projects/${ref}`).catch((e) => {
|
|
243
|
+
console.error(e.message);
|
|
244
|
+
process.exit(1);
|
|
245
|
+
});
|
|
246
|
+
printOutput(res.project, opts.output, () => {
|
|
247
|
+
kv([["id", res.project.shortId], ["slug", res.project.slug], ["name", res.project.name], ["full_id", res.project.id]]);
|
|
248
|
+
}, "slug");
|
|
249
|
+
})
|
|
250
|
+
);
|
|
251
|
+
cmd.addCommand(
|
|
252
|
+
new Command("update").argument("<ref>").description("Update a project").option("--name <name>", "New display name").option("--slug <new-slug>", "New URL slug").option("--org <slug>", "Override active org").option("--ws <slug>", "Override active workspace").action(async (ref, opts) => {
|
|
253
|
+
if (!opts.name && !opts.slug) {
|
|
254
|
+
warn("Provide at least --name or --slug");
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
const orgSlug = activeOrgSlug(ctx, cli, opts.org);
|
|
258
|
+
const wsSlug = activeWsSlug(ctx, cli, opts.ws);
|
|
259
|
+
const body = {};
|
|
260
|
+
if (opts.name) body["name"] = opts.name;
|
|
261
|
+
if (opts.slug) body["slug"] = opts.slug;
|
|
262
|
+
const res = await http.patch(`/v1/orgs/${orgSlug}/workspaces/${wsSlug}/projects/${ref}`, body).catch((e) => {
|
|
263
|
+
console.error(e.message);
|
|
264
|
+
process.exit(1);
|
|
265
|
+
});
|
|
266
|
+
success("Project updated.");
|
|
267
|
+
kv([["id", res.project.shortId], ["slug", res.project.slug], ["name", res.project.name]]);
|
|
268
|
+
})
|
|
269
|
+
);
|
|
270
|
+
cmd.addCommand(
|
|
271
|
+
new Command("delete").argument("<ref>").description("Delete a project").option("--org <slug>", "Override active org").option("--ws <slug>", "Override active workspace").action(async (ref, opts) => {
|
|
272
|
+
const orgSlug = activeOrgSlug(ctx, cli, opts.org);
|
|
273
|
+
const wsSlug = activeWsSlug(ctx, cli, opts.ws);
|
|
274
|
+
await http.del(`/v1/orgs/${orgSlug}/workspaces/${wsSlug}/projects/${ref}`).catch((e) => {
|
|
275
|
+
console.error(e.message);
|
|
276
|
+
process.exit(1);
|
|
277
|
+
});
|
|
278
|
+
success(`Project deleted: ${ref}`);
|
|
279
|
+
})
|
|
280
|
+
);
|
|
281
|
+
return cmd;
|
|
282
|
+
}
|
|
283
|
+
function buildEnvCommand(deps) {
|
|
284
|
+
const { http, ctx } = deps;
|
|
285
|
+
const cli = deps.cliName ?? "cli";
|
|
286
|
+
function basePath(orgSlug, wsSlug, projSlug) {
|
|
287
|
+
return `/v1/orgs/${orgSlug}/workspaces/${wsSlug}/projects/${projSlug}/envs`;
|
|
288
|
+
}
|
|
289
|
+
const cmd = new Command("env").description("Environment management").enablePositionalOptions().option("--org <slug>", "Override active org").option("--ws <slug>", "Override active workspace").option("--proj <slug>", "Override active project").option(...outputOption()).action(async (opts) => {
|
|
290
|
+
const orgSlug = activeOrgSlug(ctx, cli, opts.org);
|
|
291
|
+
const wsSlug = activeWsSlug(ctx, cli, opts.ws);
|
|
292
|
+
const projSlug = activeProjSlug(ctx, cli, opts.proj);
|
|
293
|
+
const res = await http.get(basePath(orgSlug, wsSlug, projSlug)).catch((e) => {
|
|
294
|
+
console.error(e.message);
|
|
295
|
+
process.exit(1);
|
|
296
|
+
});
|
|
297
|
+
printOutput(res.envs, opts.output, (wide) => {
|
|
298
|
+
table(res.envs, [
|
|
299
|
+
{ key: "shortId", label: "ID" },
|
|
300
|
+
{ key: "slug", label: "SLUG" },
|
|
301
|
+
{ key: "name", label: "NAME" },
|
|
302
|
+
{ key: "isDefault", label: "DEFAULT", fmt: (v) => v ? "\u2713" : "" },
|
|
303
|
+
{ key: "id", label: "FULL ID", wide: true }
|
|
304
|
+
], { wide, emptyHint: `No envs yet. Run: ${cli} env create --name <n>` });
|
|
305
|
+
summary(`${res.envs.length} env${res.envs.length !== 1 ? "s" : ""}`);
|
|
306
|
+
}, "slug");
|
|
307
|
+
});
|
|
308
|
+
cmd.addCommand(
|
|
309
|
+
new Command("create").description("Create an environment").requiredOption("--name <name>", "Display name").option("--slug <slug>", "URL slug (auto-generated if omitted)").option("--org <slug>", "Override active org").option("--ws <slug>", "Override active workspace").option("--proj <slug>", "Override active project").action(async (opts) => {
|
|
310
|
+
const orgSlug = activeOrgSlug(ctx, cli, opts.org);
|
|
311
|
+
const wsSlug = activeWsSlug(ctx, cli, opts.ws);
|
|
312
|
+
const projSlug = activeProjSlug(ctx, cli, opts.proj);
|
|
313
|
+
const res = await http.post(basePath(orgSlug, wsSlug, projSlug), { name: opts.name, slug: opts.slug }).catch((e) => {
|
|
314
|
+
console.error(e.message);
|
|
315
|
+
process.exit(1);
|
|
316
|
+
});
|
|
317
|
+
success(`Env created: ${res.env.slug}`);
|
|
318
|
+
kv([["id", res.env.shortId], ["slug", res.env.slug]]);
|
|
319
|
+
})
|
|
320
|
+
);
|
|
321
|
+
cmd.addCommand(
|
|
322
|
+
new Command("get").argument("<ref>").description("Get environment details").option("--org <slug>", "Override active org").option("--ws <slug>", "Override active workspace").option("--proj <slug>", "Override active project").option(...outputOption()).action(async (ref, opts) => {
|
|
323
|
+
const orgSlug = activeOrgSlug(ctx, cli, opts.org);
|
|
324
|
+
const wsSlug = activeWsSlug(ctx, cli, opts.ws);
|
|
325
|
+
const projSlug = activeProjSlug(ctx, cli, opts.proj);
|
|
326
|
+
const res = await http.get(`${basePath(orgSlug, wsSlug, projSlug)}/${ref}`).catch((e) => {
|
|
327
|
+
console.error(e.message);
|
|
328
|
+
process.exit(1);
|
|
329
|
+
});
|
|
330
|
+
printOutput(res.env, opts.output, () => {
|
|
331
|
+
kv([["id", res.env.shortId], ["slug", res.env.slug], ["name", res.env.name], ["full_id", res.env.id]]);
|
|
332
|
+
}, "slug");
|
|
333
|
+
})
|
|
334
|
+
);
|
|
335
|
+
cmd.addCommand(
|
|
336
|
+
new Command("update").argument("<ref>").description("Update an environment").option("--name <name>", "New display name").option("--slug <new-slug>", "New URL slug").option("--org <slug>", "Override active org").option("--ws <slug>", "Override active workspace").option("--proj <slug>", "Override active project").action(async (ref, opts) => {
|
|
337
|
+
if (!opts.name && !opts.slug) {
|
|
338
|
+
warn("Provide at least --name or --slug");
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
const orgSlug = activeOrgSlug(ctx, cli, opts.org);
|
|
342
|
+
const wsSlug = activeWsSlug(ctx, cli, opts.ws);
|
|
343
|
+
const projSlug = activeProjSlug(ctx, cli, opts.proj);
|
|
344
|
+
const body = {};
|
|
345
|
+
if (opts.name) body["name"] = opts.name;
|
|
346
|
+
if (opts.slug) body["slug"] = opts.slug;
|
|
347
|
+
const res = await http.patch(`${basePath(orgSlug, wsSlug, projSlug)}/${ref}`, body).catch((e) => {
|
|
348
|
+
console.error(e.message);
|
|
349
|
+
process.exit(1);
|
|
350
|
+
});
|
|
351
|
+
success("Env updated.");
|
|
352
|
+
kv([["id", res.env.shortId], ["slug", res.env.slug], ["name", res.env.name]]);
|
|
353
|
+
})
|
|
354
|
+
);
|
|
355
|
+
cmd.addCommand(
|
|
356
|
+
new Command("delete").argument("<ref>").description("Delete an environment").option("--org <slug>", "Override active org").option("--ws <slug>", "Override active workspace").option("--proj <slug>", "Override active project").action(async (ref, opts) => {
|
|
357
|
+
const orgSlug = activeOrgSlug(ctx, cli, opts.org);
|
|
358
|
+
const wsSlug = activeWsSlug(ctx, cli, opts.ws);
|
|
359
|
+
const projSlug = activeProjSlug(ctx, cli, opts.proj);
|
|
360
|
+
await http.del(`${basePath(orgSlug, wsSlug, projSlug)}/${ref}`).catch((e) => {
|
|
361
|
+
console.error(e.message);
|
|
362
|
+
process.exit(1);
|
|
363
|
+
});
|
|
364
|
+
success(`Env deleted: ${ref}`);
|
|
365
|
+
})
|
|
366
|
+
);
|
|
367
|
+
return cmd;
|
|
368
|
+
}
|
|
369
|
+
function addGlobalCommands(program, deps) {
|
|
370
|
+
const { http, ctx } = deps;
|
|
371
|
+
const cli = deps.cliName ?? "cli";
|
|
372
|
+
program.addCommand(
|
|
373
|
+
new Command("use").alias("context").description("Set active org / workspace / project (interactive if no arg)").argument("[path]", "org or org/ws or org/ws/proj").action(async (path) => {
|
|
374
|
+
await runUseCommand(http, ctx, path);
|
|
375
|
+
})
|
|
376
|
+
);
|
|
377
|
+
program.addCommand(
|
|
378
|
+
new Command("ls").description("Tree view of all orgs, workspaces, and projects").action(async () => {
|
|
379
|
+
const ctxData = ctx.getContext();
|
|
380
|
+
const activeOrg = ctxData?.["org"];
|
|
381
|
+
const activeWs = ctxData?.["workspace"];
|
|
382
|
+
const activeProj = ctxData?.["project"];
|
|
383
|
+
const { orgs } = await http.get("/v1/orgs").catch((e) => {
|
|
384
|
+
console.error(e.message);
|
|
385
|
+
process.exit(1);
|
|
386
|
+
});
|
|
387
|
+
let totalWs = 0;
|
|
388
|
+
let totalProj = 0;
|
|
389
|
+
const nodes = await Promise.all(
|
|
390
|
+
orgs.map(async (org) => {
|
|
391
|
+
const { workspaces } = await http.get(`/v1/orgs/${org.slug}/workspaces`);
|
|
392
|
+
totalWs += workspaces.length;
|
|
393
|
+
const wsNodes = await Promise.all(
|
|
394
|
+
workspaces.map(async (ws) => {
|
|
395
|
+
const { projects } = await http.get(`/v1/orgs/${org.slug}/workspaces/${ws.slug}/projects`);
|
|
396
|
+
totalProj += projects.length;
|
|
397
|
+
const isActiveWs = org.slug === activeOrg && ws.slug === activeWs;
|
|
398
|
+
return {
|
|
399
|
+
label: isActiveWs ? clr.bold + ws.slug + clr.reset : ws.slug,
|
|
400
|
+
badge: isActiveWs ? clr.cyan + "\u25CF" + clr.reset : void 0,
|
|
401
|
+
children: projects.map((p) => {
|
|
402
|
+
const isActiveProj = isActiveWs && p.slug === activeProj;
|
|
403
|
+
return {
|
|
404
|
+
label: isActiveProj ? clr.bold + p.slug + clr.reset : p.slug,
|
|
405
|
+
badge: isActiveProj ? clr.green + "\u25CF" + clr.reset : void 0
|
|
406
|
+
};
|
|
407
|
+
})
|
|
408
|
+
};
|
|
409
|
+
})
|
|
410
|
+
);
|
|
411
|
+
const isActiveOrg = org.slug === activeOrg;
|
|
412
|
+
return {
|
|
413
|
+
label: isActiveOrg ? clr.bold + org.slug + clr.reset : org.slug,
|
|
414
|
+
badge: isActiveOrg ? clr.cyan + "\u2605" + clr.reset : void 0,
|
|
415
|
+
meta: org.name && org.name !== org.slug ? org.name : void 0,
|
|
416
|
+
children: wsNodes
|
|
417
|
+
};
|
|
418
|
+
})
|
|
419
|
+
);
|
|
420
|
+
console.log();
|
|
421
|
+
tree(nodes);
|
|
422
|
+
console.log();
|
|
423
|
+
console.log(
|
|
424
|
+
` ${clr.dim}${orgs.length} org${orgs.length !== 1 ? "s" : ""} \xB7 ${totalWs} workspace${totalWs !== 1 ? "s" : ""} \xB7 ${totalProj} project${totalProj !== 1 ? "s" : ""}` + (activeOrg ? ` \xB7 active: ${clr.bold}${activeOrg}/${activeWs}/${activeProj}${clr.reset}${clr.dim}` : "") + clr.reset
|
|
425
|
+
);
|
|
426
|
+
console.log();
|
|
427
|
+
})
|
|
428
|
+
);
|
|
429
|
+
program.addCommand(
|
|
430
|
+
new Command("ps").description("Show active session (context, token, api endpoint)").action(() => {
|
|
431
|
+
const ctxData = ctx.getContext();
|
|
432
|
+
const cfg = ctx.loadConfig();
|
|
433
|
+
const token = cfg["token"] ?? Object.values(cfg["orgs"] ?? {}).find((e) => e?.token)?.token;
|
|
434
|
+
const apiBase = cfg["apiBase"] ?? deps.appBase;
|
|
435
|
+
console.log();
|
|
436
|
+
if (ctxData?.["org"]) {
|
|
437
|
+
console.log(
|
|
438
|
+
` ${clr.dim}context${clr.reset} ${clr.bold}${ctxData["org"]}${clr.reset} ${clr.dim}/${clr.reset} ${ctxData["workspace"] ?? "\u2014"} ${clr.dim}/${clr.reset} ${ctxData["project"] ?? "\u2014"} ${clr.green}\u25CF active${clr.reset}`
|
|
439
|
+
);
|
|
440
|
+
} else {
|
|
441
|
+
console.log(` ${clr.dim}context${clr.reset} ${clr.yellow}not set${clr.reset} ${clr.dim}(run: ${cli} use)${clr.reset}`);
|
|
442
|
+
}
|
|
443
|
+
if (token) {
|
|
444
|
+
console.log(` ${clr.dim}token ${clr.reset} ${clr.cyan}${token.slice(0, 14)}\u2026${clr.reset}`);
|
|
445
|
+
} else {
|
|
446
|
+
console.log(` ${clr.dim}token ${clr.reset} ${clr.yellow}none${clr.reset} ${clr.dim}(run: ${cli} login)${clr.reset}`);
|
|
447
|
+
}
|
|
448
|
+
console.log(` ${clr.dim}api ${clr.reset} ${clr.dim}${apiBase}${clr.reset}`);
|
|
449
|
+
console.log(` ${clr.dim}config ${clr.reset} ${clr.dim}${ctx.configPath}${clr.reset}`);
|
|
450
|
+
console.log();
|
|
451
|
+
})
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
function orgCommand(deps) {
|
|
455
|
+
const root = new Command("org").description("Org, workspace, and project management");
|
|
456
|
+
addGlobalCommands(root, deps);
|
|
457
|
+
root.addCommand(buildOrgCommand(deps));
|
|
458
|
+
root.addCommand(buildWsCommand(deps));
|
|
459
|
+
root.addCommand(buildProjCommand(deps));
|
|
460
|
+
return root;
|
|
461
|
+
}
|
|
462
|
+
async function runUseCommand(http, ctx, path) {
|
|
463
|
+
async function pick(label, items) {
|
|
464
|
+
if (!items.length) fatal(`No ${label}s found.`);
|
|
465
|
+
if (items.length === 1) return items[0];
|
|
466
|
+
console.log(`
|
|
467
|
+
Select ${label}:`);
|
|
468
|
+
items.forEach((it, i) => console.log(` ${clr.cyan}${i + 1}${clr.reset} ${it.slug}${it.name ? ` ${clr.dim}${it.name}${clr.reset}` : ""}`));
|
|
469
|
+
const ans = await prompt(`${label} [1-${items.length}]: `);
|
|
470
|
+
const n = parseInt(ans, 10);
|
|
471
|
+
if (!n || n < 1 || n > items.length) fatal("Invalid selection.");
|
|
472
|
+
return items[n - 1];
|
|
473
|
+
}
|
|
474
|
+
const parts = (path ?? "").split("/").map((s) => s.trim()).filter(Boolean);
|
|
475
|
+
const orgHint = parts[0];
|
|
476
|
+
const wsHint = parts[1];
|
|
477
|
+
const projHint = parts[2];
|
|
478
|
+
let orgSlug;
|
|
479
|
+
if (orgHint) {
|
|
480
|
+
orgSlug = orgHint;
|
|
481
|
+
} else {
|
|
482
|
+
const { orgs } = await http.get("/v1/orgs").catch((e) => {
|
|
483
|
+
console.error(e.message);
|
|
484
|
+
process.exit(1);
|
|
485
|
+
});
|
|
486
|
+
const org = await pick("org", orgs);
|
|
487
|
+
orgSlug = org.slug;
|
|
488
|
+
}
|
|
489
|
+
const orgRes = await http.get(`/v1/orgs/${orgSlug}`).catch(() => fatal(`Org not found: ${orgSlug}`));
|
|
490
|
+
if (parts.length <= 1 && orgHint) {
|
|
491
|
+
const cfg = ctx.loadConfig();
|
|
492
|
+
ctx.saveConfig({ ...cfg, activeOrg: orgRes.org.slug, context: { org: orgRes.org.slug, orgId: orgRes.org.id } });
|
|
493
|
+
success("Active org set.");
|
|
494
|
+
kv([["org", orgRes.org.slug]]);
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
const { workspaces } = await http.get(`/v1/orgs/${orgSlug}/workspaces`).catch((e) => {
|
|
498
|
+
console.error(e.message);
|
|
499
|
+
process.exit(1);
|
|
500
|
+
});
|
|
501
|
+
const ws = wsHint ? workspaces.find((w) => w.slug === wsHint) ?? fatal(`Workspace not found: ${wsHint}`) : workspaces.length === 1 ? workspaces[0] : await pick("workspace", workspaces);
|
|
502
|
+
if (parts.length === 2) {
|
|
503
|
+
const cfg = ctx.loadConfig();
|
|
504
|
+
ctx.saveConfig({ ...cfg, activeOrg: orgRes.org.slug });
|
|
505
|
+
ctx.setContext({ org: orgRes.org.slug, orgId: orgRes.org.id, workspace: ws.slug, workspaceId: ws.id });
|
|
506
|
+
success("Active context set.");
|
|
507
|
+
kv([["org", orgRes.org.slug], ["workspace", ws.slug]]);
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
const projRes = await http.get(`/v1/orgs/${orgSlug}/workspaces/${ws.slug}/projects`).catch((e) => {
|
|
511
|
+
console.error(e.message);
|
|
512
|
+
process.exit(1);
|
|
513
|
+
});
|
|
514
|
+
const { projects } = projRes;
|
|
515
|
+
const proj = projHint ? projects.find((p) => p.slug === projHint) ?? fatal(`Project not found: ${projHint}`) : projects.length === 1 ? projects[0] : await pick("project", projects);
|
|
516
|
+
ctx.setContext({
|
|
517
|
+
org: orgRes.org.slug,
|
|
518
|
+
orgId: orgRes.org.id,
|
|
519
|
+
workspace: ws.slug,
|
|
520
|
+
workspaceId: ws.id,
|
|
521
|
+
project: proj.slug,
|
|
522
|
+
projectId: proj.id
|
|
523
|
+
});
|
|
524
|
+
success("Active context set.");
|
|
525
|
+
kv([["org", orgRes.org.slug], ["workspace", ws.slug], ["project", proj.slug]]);
|
|
526
|
+
}
|
|
527
|
+
export {
|
|
528
|
+
addGlobalCommands,
|
|
529
|
+
buildEnvCommand,
|
|
530
|
+
buildOrgCommand,
|
|
531
|
+
buildProjCommand,
|
|
532
|
+
buildWsCommand,
|
|
533
|
+
orgCommand
|
|
534
|
+
};
|