zinkee 0.1.44 → 0.1.45
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/README.md +4 -0
- package/dist/chunk-MHEVRMDX.js +1230 -0
- package/dist/chunk-MHEVRMDX.js.map +1 -0
- package/{src/utils/examples.ts → dist/examples-E6FFEVBE.js} +856 -1141
- package/dist/examples-E6FFEVBE.js.map +1 -0
- package/dist/index.js +1145 -8456
- package/dist/index.js.map +1 -1
- package/package.json +6 -3
- package/.github/workflows/npm-publish.yml +0 -77
- package/.github/workflows/pr-checks.yml +0 -36
- package/AGENTS.md +0 -110
- package/docs/cli-contract.md +0 -41
- package/docs/npm-release.md +0 -64
- package/docs/superpowers/plans/2026-03-24-zinkee-cli-implementation.md +0 -837
- package/docs/superpowers/plans/2026-03-25-cli-backend-error-contract.md +0 -503
- package/docs/superpowers/plans/2026-03-26-display-freeform-create.md +0 -389
- package/docs/superpowers/specs/2026-03-24-zinkee-cli-backend-blockers.md +0 -172
- package/docs/superpowers/specs/2026-03-24-zinkee-cli-design.md +0 -1576
- package/docs/superpowers/specs/2026-03-24-zinkee-cli-e2e-checklist.md +0 -215
- package/docs/superpowers/specs/2026-03-24-zinkee-cli-e2e-design.md +0 -492
- package/docs/superpowers/specs/2026-03-24-zinkee-cli-e2e-status.md +0 -307
- package/docs/superpowers/specs/2026-07-30-cli-pr-checks-design.md +0 -37
- package/src/api/automations.ts +0 -404
- package/src/api/comments.ts +0 -51
- package/src/api/displays.ts +0 -337
- package/src/api/document-templates.ts +0 -110
- package/src/api/files.ts +0 -50
- package/src/api/formulas.test.ts +0 -65
- package/src/api/formulas.ts +0 -67
- package/src/api/logs.ts +0 -34
- package/src/api/navigation.ts +0 -70
- package/src/api/records.ts +0 -184
- package/src/api/schemas.ts +0 -148
- package/src/api/teamspace.ts +0 -110
- package/src/cli-examples.ts +0 -130
- package/src/cli-runner.ts +0 -95
- package/src/client.test.ts +0 -189
- package/src/client.ts +0 -269
- package/src/command-registry.ts +0 -882
- package/src/commands/automations.test.ts +0 -1030
- package/src/commands/automations.ts +0 -2102
- package/src/commands/comments.test.ts +0 -214
- package/src/commands/comments.ts +0 -303
- package/src/commands/config.test.ts +0 -81
- package/src/commands/config.ts +0 -150
- package/src/commands/displays.test.ts +0 -1105
- package/src/commands/displays.ts +0 -1442
- package/src/commands/document-templates.test.ts +0 -569
- package/src/commands/document-templates.ts +0 -563
- package/src/commands/files.test.ts +0 -284
- package/src/commands/files.ts +0 -280
- package/src/commands/formulas.test.ts +0 -194
- package/src/commands/formulas.ts +0 -243
- package/src/commands/logs.test.ts +0 -123
- package/src/commands/logs.ts +0 -159
- package/src/commands/navigation.test.ts +0 -211
- package/src/commands/navigation.ts +0 -348
- package/src/commands/profiles.test.ts +0 -191
- package/src/commands/profiles.ts +0 -303
- package/src/commands/records.test.ts +0 -860
- package/src/commands/records.ts +0 -883
- package/src/commands/schemas.test.ts +0 -1252
- package/src/commands/schemas.ts +0 -890
- package/src/commands/teamspace.test.ts +0 -229
- package/src/commands/teamspace.ts +0 -546
- package/src/completion/engine.test.ts +0 -138
- package/src/completion/engine.ts +0 -168
- package/src/completion/install.test.ts +0 -179
- package/src/completion/install.ts +0 -260
- package/src/completion/runtime.ts +0 -150
- package/src/completion/scripts.ts +0 -91
- package/src/config.test.ts +0 -362
- package/src/config.ts +0 -294
- package/src/index.test.ts +0 -217
- package/src/index.ts +0 -8
- package/src/parsers/expressions.test.ts +0 -95
- package/src/parsers/expressions.ts +0 -128
- package/src/parsers/kv.test.ts +0 -35
- package/src/parsers/kv.ts +0 -49
- package/src/parsers/selectors.test.ts +0 -23
- package/src/parsers/selectors.ts +0 -29
- package/src/program.ts +0 -64
- package/src/runtime-context.ts +0 -103
- package/src/types.ts +0 -76
- package/src/utils/argv-rewrite.test.ts +0 -149
- package/src/utils/argv-rewrite.ts +0 -269
- package/src/utils/errors.test.ts +0 -85
- package/src/utils/errors.ts +0 -191
- package/src/utils/examples.test.ts +0 -374
- package/src/utils/output.test.ts +0 -65
- package/src/utils/output.ts +0 -154
- package/src/utils/schema-fields.ts +0 -1016
- package/tsconfig.json +0 -20
- package/tsup.config.ts +0 -13
|
@@ -1,211 +0,0 @@
|
|
|
1
|
-
import { mkdtempSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { tmpdir } from "node:os";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
|
|
5
|
-
import { Command } from "commander";
|
|
6
|
-
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
7
|
-
|
|
8
|
-
import { registerNavigationCommands } from "./navigation.js";
|
|
9
|
-
|
|
10
|
-
function createTempConfig(contents: string): string {
|
|
11
|
-
const dir = mkdtempSync(join(tmpdir(), "zinkee-navigation-"));
|
|
12
|
-
const filePath = join(dir, "config.toml");
|
|
13
|
-
writeFileSync(filePath, contents, "utf8");
|
|
14
|
-
return filePath;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function createApiConfig(): string {
|
|
18
|
-
return createTempConfig(`
|
|
19
|
-
default_profile = "prod"
|
|
20
|
-
|
|
21
|
-
[profiles.prod]
|
|
22
|
-
api_key = "navigation-token-1234"
|
|
23
|
-
base_url = "https://api.zinkee.test"
|
|
24
|
-
`);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function createStdoutBuffer() {
|
|
28
|
-
let output = "";
|
|
29
|
-
|
|
30
|
-
return {
|
|
31
|
-
stdout: {
|
|
32
|
-
write(chunk: string) {
|
|
33
|
-
output += chunk;
|
|
34
|
-
return true;
|
|
35
|
-
},
|
|
36
|
-
},
|
|
37
|
-
read() {
|
|
38
|
-
return output;
|
|
39
|
-
},
|
|
40
|
-
};
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function jsonResponse(body: unknown, status: number = 200): Response {
|
|
44
|
-
return new Response(JSON.stringify(body), {
|
|
45
|
-
status,
|
|
46
|
-
headers: {
|
|
47
|
-
"content-type": "application/json",
|
|
48
|
-
},
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function createFetchMock() {
|
|
53
|
-
const fetchMock = vi.fn<typeof fetch>();
|
|
54
|
-
vi.stubGlobal("fetch", fetchMock);
|
|
55
|
-
return fetchMock;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function buildProgram(configPath: string, stdout: { write(chunk: string): unknown }) {
|
|
59
|
-
const program = new Command();
|
|
60
|
-
|
|
61
|
-
program
|
|
62
|
-
.name("zinkee")
|
|
63
|
-
.enablePositionalOptions()
|
|
64
|
-
.showHelpAfterError()
|
|
65
|
-
.option("--profile <name>", "select a profile from config")
|
|
66
|
-
.option("--base-url <url>", "override the configured base URL")
|
|
67
|
-
.option("--api-key <token>", "override the configured API key")
|
|
68
|
-
.option("--read-only", "run in read-only mode")
|
|
69
|
-
.option("--json", "emit JSON output");
|
|
70
|
-
|
|
71
|
-
registerNavigationCommands(program, { configPath, stdout });
|
|
72
|
-
|
|
73
|
-
return program;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
afterEach(() => {
|
|
77
|
-
vi.unstubAllGlobals();
|
|
78
|
-
vi.restoreAllMocks();
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
describe("navigation commands", () => {
|
|
82
|
-
it("resolves exact folder names for get and rejects duplicate matches", async () => {
|
|
83
|
-
const configPath = createApiConfig();
|
|
84
|
-
const output = createStdoutBuffer();
|
|
85
|
-
const fetchMock = createFetchMock();
|
|
86
|
-
fetchMock.mockResolvedValueOnce(
|
|
87
|
-
jsonResponse([
|
|
88
|
-
{
|
|
89
|
-
id: "11111111-1111-1111-1111-111111111111",
|
|
90
|
-
name: "Finance",
|
|
91
|
-
order: 10,
|
|
92
|
-
resources: [],
|
|
93
|
-
},
|
|
94
|
-
{
|
|
95
|
-
id: "22222222-2222-2222-2222-222222222222",
|
|
96
|
-
name: "Marketing",
|
|
97
|
-
order: 20,
|
|
98
|
-
resources: [],
|
|
99
|
-
},
|
|
100
|
-
]),
|
|
101
|
-
);
|
|
102
|
-
const program = buildProgram(configPath, output.stdout);
|
|
103
|
-
|
|
104
|
-
await program.parseAsync([
|
|
105
|
-
"node",
|
|
106
|
-
"zinkee",
|
|
107
|
-
"--json",
|
|
108
|
-
"navigation",
|
|
109
|
-
"folders",
|
|
110
|
-
"get",
|
|
111
|
-
"Finance",
|
|
112
|
-
]);
|
|
113
|
-
|
|
114
|
-
expect(fetchMock.mock.calls[0]?.[0]).toBe(
|
|
115
|
-
"https://api.zinkee.test/api/v2/navigation/folders",
|
|
116
|
-
);
|
|
117
|
-
expect(JSON.parse(output.read())).toEqual({
|
|
118
|
-
data: {
|
|
119
|
-
id: "11111111-1111-1111-1111-111111111111",
|
|
120
|
-
name: "Finance",
|
|
121
|
-
order: 10,
|
|
122
|
-
resources: [],
|
|
123
|
-
},
|
|
124
|
-
meta: {
|
|
125
|
-
command: "navigation folders get",
|
|
126
|
-
configPath,
|
|
127
|
-
},
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
fetchMock.mockReset();
|
|
131
|
-
fetchMock.mockResolvedValueOnce(
|
|
132
|
-
jsonResponse([
|
|
133
|
-
{
|
|
134
|
-
id: "11111111-1111-1111-1111-111111111111",
|
|
135
|
-
name: "Finance",
|
|
136
|
-
order: 10,
|
|
137
|
-
resources: [],
|
|
138
|
-
},
|
|
139
|
-
{
|
|
140
|
-
id: "22222222-2222-2222-2222-222222222222",
|
|
141
|
-
name: "Finance",
|
|
142
|
-
order: 20,
|
|
143
|
-
resources: [],
|
|
144
|
-
},
|
|
145
|
-
]),
|
|
146
|
-
);
|
|
147
|
-
|
|
148
|
-
await expect(
|
|
149
|
-
buildProgram(configPath, output.stdout).parseAsync([
|
|
150
|
-
"node",
|
|
151
|
-
"zinkee",
|
|
152
|
-
"navigation",
|
|
153
|
-
"folders",
|
|
154
|
-
"get",
|
|
155
|
-
"Finance",
|
|
156
|
-
]),
|
|
157
|
-
).rejects.toThrowError('Folder "Finance" is ambiguous. Found 2 matches.');
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
it("shapes move payloads using the resolved folder id (schemas only, no resource type)", async () => {
|
|
161
|
-
const configPath = createApiConfig();
|
|
162
|
-
const output = createStdoutBuffer();
|
|
163
|
-
const fetchMock = createFetchMock();
|
|
164
|
-
fetchMock.mockResolvedValueOnce(
|
|
165
|
-
jsonResponse([
|
|
166
|
-
{
|
|
167
|
-
id: "aaaabbbb-cccc-dddd-eeee-ffffffffffff",
|
|
168
|
-
name: "Finance",
|
|
169
|
-
order: 10,
|
|
170
|
-
resources: [
|
|
171
|
-
{
|
|
172
|
-
id: "resource-1",
|
|
173
|
-
name: "Invoices",
|
|
174
|
-
type: "SCHEMA",
|
|
175
|
-
isInSpace: false,
|
|
176
|
-
},
|
|
177
|
-
],
|
|
178
|
-
},
|
|
179
|
-
]),
|
|
180
|
-
);
|
|
181
|
-
fetchMock.mockResolvedValueOnce(jsonResponse({ resourceId: "resource-1", targetFolderId: "aaaabbbb-cccc-dddd-eeee-ffffffffffff" }));
|
|
182
|
-
const program = buildProgram(configPath, output.stdout);
|
|
183
|
-
|
|
184
|
-
await program.parseAsync([
|
|
185
|
-
"node",
|
|
186
|
-
"zinkee",
|
|
187
|
-
"--json",
|
|
188
|
-
"navigation",
|
|
189
|
-
"resources",
|
|
190
|
-
"move",
|
|
191
|
-
"resource-1",
|
|
192
|
-
"--to",
|
|
193
|
-
"Finance",
|
|
194
|
-
]);
|
|
195
|
-
|
|
196
|
-
expect(fetchMock.mock.calls[0]?.[0]).toBe(
|
|
197
|
-
"https://api.zinkee.test/api/v2/navigation/folders",
|
|
198
|
-
);
|
|
199
|
-
|
|
200
|
-
const moveInit = fetchMock.mock.calls[1]?.[1];
|
|
201
|
-
|
|
202
|
-
expect(fetchMock.mock.calls[1]?.[0]).toBe(
|
|
203
|
-
"https://api.zinkee.test/api/v2/navigation/resources:move",
|
|
204
|
-
);
|
|
205
|
-
expect(moveInit?.method).toBe("POST");
|
|
206
|
-
expect(JSON.parse(String(moveInit?.body))).toEqual({
|
|
207
|
-
resourceId: "resource-1",
|
|
208
|
-
targetFolderId: "aaaabbbb-cccc-dddd-eeee-ffffffffffff",
|
|
209
|
-
});
|
|
210
|
-
});
|
|
211
|
-
});
|
|
@@ -1,348 +0,0 @@
|
|
|
1
|
-
import { readFileSync } from "node:fs";
|
|
2
|
-
import type { Writable } from "node:stream";
|
|
3
|
-
|
|
4
|
-
import { Command, InvalidArgumentError } from "commander";
|
|
5
|
-
|
|
6
|
-
import { NavigationApi, type NavigationFolder } from "../api/navigation.js";
|
|
7
|
-
import { ApiClient } from "../client.js";
|
|
8
|
-
import { defaultConfigPath, readRuntimeConfig } from "../config.js";
|
|
9
|
-
import { normalizeUuidOrSlug, type NormalizedUuidOrSlug } from "../parsers/selectors.js";
|
|
10
|
-
import { resolveRuntimeContext } from "../runtime-context.js";
|
|
11
|
-
import type { RuntimeOverrides } from "../types.js";
|
|
12
|
-
import { CliError } from "../utils/errors.js";
|
|
13
|
-
import { buildJsonSuccess, printTable } from "../utils/output.js";
|
|
14
|
-
|
|
15
|
-
interface CommandIo {
|
|
16
|
-
write(chunk: string): unknown;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export interface RegisterNavigationCommandsOptions {
|
|
20
|
-
configPath?: string;
|
|
21
|
-
stdout?: CommandIo;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
interface GlobalOptions extends RuntimeOverrides {}
|
|
25
|
-
|
|
26
|
-
interface FolderMutationOptions {
|
|
27
|
-
name?: string;
|
|
28
|
-
order?: string;
|
|
29
|
-
props?: string;
|
|
30
|
-
propsFile?: string;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function getCommandIo(stdout: CommandIo | undefined): CommandIo {
|
|
34
|
-
return stdout ?? (process.stdout as Writable);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function writeLine(stdout: CommandIo, value: string): void {
|
|
38
|
-
stdout.write(`${value}\n`);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function getGlobalOptions(command: Command): GlobalOptions {
|
|
42
|
-
return command.optsWithGlobals<GlobalOptions>();
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function emitJson(
|
|
46
|
-
stdout: CommandIo,
|
|
47
|
-
commandName: string,
|
|
48
|
-
data: unknown,
|
|
49
|
-
configPath: string,
|
|
50
|
-
): void {
|
|
51
|
-
writeLine(stdout, JSON.stringify(buildJsonSuccess(data, { command: commandName, configPath })));
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function toConfigurationError(filePath: string, error: unknown): never {
|
|
55
|
-
if (error instanceof CliError) {
|
|
56
|
-
throw error;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") {
|
|
60
|
-
throw new CliError("configuration_error", `Config file not found at ${filePath}.`);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
if (error instanceof Error) {
|
|
64
|
-
throw new CliError("configuration_error", error.message);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
throw error;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function createNavigationApi(
|
|
71
|
-
command: Command,
|
|
72
|
-
filePath: string,
|
|
73
|
-
): { api: NavigationApi; json: boolean; readOnly: boolean } {
|
|
74
|
-
try {
|
|
75
|
-
const config = readRuntimeConfig(filePath);
|
|
76
|
-
const runtime = resolveRuntimeContext(config, getGlobalOptions(command));
|
|
77
|
-
|
|
78
|
-
if (!runtime.baseUrl) {
|
|
79
|
-
throw new CliError("configuration_error", `Resolved profile "${runtime.profile}" is missing a base URL.`);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
return {
|
|
83
|
-
api: new NavigationApi(
|
|
84
|
-
new ApiClient({
|
|
85
|
-
baseUrl: runtime.baseUrl,
|
|
86
|
-
apiKey: runtime.apiKey,
|
|
87
|
-
}),
|
|
88
|
-
),
|
|
89
|
-
json: runtime.json,
|
|
90
|
-
readOnly: runtime.readOnly,
|
|
91
|
-
};
|
|
92
|
-
} catch (error) {
|
|
93
|
-
toConfigurationError(filePath, error);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function ensureWritable(commandName: string, readOnly: boolean, mode: "write" | "destructive"): void {
|
|
98
|
-
if (readOnly) {
|
|
99
|
-
throw new CliError(
|
|
100
|
-
"command_not_allowed_in_read_only_mode",
|
|
101
|
-
`Command "${commandName}" is not allowed in read-only mode.`,
|
|
102
|
-
{ commandMode: mode, executionMode: "read-only" },
|
|
103
|
-
);
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function parseUuidOrSlugSelector(input: string): NormalizedUuidOrSlug {
|
|
108
|
-
try {
|
|
109
|
-
return normalizeUuidOrSlug(input);
|
|
110
|
-
} catch (error) {
|
|
111
|
-
const message = error instanceof Error ? error.message : "Invalid selector.";
|
|
112
|
-
throw new InvalidArgumentError(message);
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function parseInteger(value: string | undefined, flagName: string): number | undefined {
|
|
117
|
-
if (value === undefined) {
|
|
118
|
-
return undefined;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
const parsed = Number.parseInt(value, 10);
|
|
122
|
-
if (Number.isNaN(parsed)) {
|
|
123
|
-
throw new CliError("invalid_cli_usage", `${flagName} must be an integer.`);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
return parsed;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
function readJsonObject(raw?: string, rawFile?: string): Record<string, unknown> {
|
|
130
|
-
if (raw && rawFile) {
|
|
131
|
-
throw new CliError("invalid_cli_usage", 'Use either "--props" or "--props-file", not both.');
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
const source = rawFile ? readFileSync(rawFile, "utf8") : raw;
|
|
135
|
-
if (!source) {
|
|
136
|
-
return {};
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
try {
|
|
140
|
-
const parsed = JSON.parse(source) as unknown;
|
|
141
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
142
|
-
throw new CliError("invalid_cli_usage", "Props payload must be a JSON object.");
|
|
143
|
-
}
|
|
144
|
-
return parsed as Record<string, unknown>;
|
|
145
|
-
} catch (error) {
|
|
146
|
-
if (error instanceof CliError) {
|
|
147
|
-
throw error;
|
|
148
|
-
}
|
|
149
|
-
throw new CliError("invalid_cli_usage", "Props payload is invalid JSON.");
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
function buildFolderBody(options: FolderMutationOptions): Record<string, unknown> {
|
|
154
|
-
const body: Record<string, unknown> = {};
|
|
155
|
-
|
|
156
|
-
if (options.name !== undefined) {
|
|
157
|
-
body.name = options.name;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
const order = parseInteger(options.order, "--order");
|
|
161
|
-
if (order !== undefined) {
|
|
162
|
-
body.order = order;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
const props = readJsonObject(options.props, options.propsFile);
|
|
166
|
-
if (Object.keys(props).length > 0) {
|
|
167
|
-
body.additionalProperties = props;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
return body;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
function resolveFolder(
|
|
174
|
-
folders: readonly NavigationFolder[],
|
|
175
|
-
selector: NormalizedUuidOrSlug,
|
|
176
|
-
): NavigationFolder {
|
|
177
|
-
if (selector.kind === "uuid") {
|
|
178
|
-
const match = folders.find((folder) => folder.id === selector.value);
|
|
179
|
-
if (!match) {
|
|
180
|
-
throw new CliError("invalid_cli_usage", `Folder "${selector.value}" was not found.`);
|
|
181
|
-
}
|
|
182
|
-
return match;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
const matches = folders.filter((folder) => folder.name === selector.value);
|
|
186
|
-
if (matches.length === 1) {
|
|
187
|
-
return matches[0] as NavigationFolder;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
if (matches.length > 1) {
|
|
191
|
-
throw new CliError(
|
|
192
|
-
"invalid_cli_usage",
|
|
193
|
-
`Folder "${selector.value}" is ambiguous. Found ${matches.length} matches.`,
|
|
194
|
-
);
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
throw new CliError("invalid_cli_usage", `Folder "${selector.value}" was not found.`);
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
function folderRows(folders: readonly NavigationFolder[]): Record<string, unknown>[] {
|
|
201
|
-
return folders.map((folder) => ({
|
|
202
|
-
id: folder.id,
|
|
203
|
-
order: folder.order ?? "",
|
|
204
|
-
name: folder.name ?? "",
|
|
205
|
-
resourceCount: Array.isArray(folder.resources) ? folder.resources.length : "",
|
|
206
|
-
}));
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
export function registerNavigationCommands(
|
|
210
|
-
program: Command,
|
|
211
|
-
options: RegisterNavigationCommandsOptions = {},
|
|
212
|
-
): void {
|
|
213
|
-
const filePath = options.configPath ?? defaultConfigPath;
|
|
214
|
-
const stdout = getCommandIo(options.stdout);
|
|
215
|
-
const navigation = program.command("navigation").description("Manage workspace navigation");
|
|
216
|
-
const folders = navigation.command("folders").description("Manage navigation folders");
|
|
217
|
-
|
|
218
|
-
folders
|
|
219
|
-
.command("list")
|
|
220
|
-
.description("List navigation folders")
|
|
221
|
-
.action(async function action(this: Command) {
|
|
222
|
-
const { api, json } = createNavigationApi(this, filePath);
|
|
223
|
-
const data = await api.listFolders();
|
|
224
|
-
|
|
225
|
-
if (json) {
|
|
226
|
-
emitJson(stdout, "navigation folders list", data, filePath);
|
|
227
|
-
return;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
printTable(folderRows(data), ["id", "order", "name", "resourceCount"], stdout);
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
folders
|
|
234
|
-
.command("get")
|
|
235
|
-
.description("Get a navigation folder by UUID or exact name")
|
|
236
|
-
.argument("<folder>", "folder UUID or exact name", parseUuidOrSlugSelector)
|
|
237
|
-
.action(async function action(this: Command, folder: NormalizedUuidOrSlug) {
|
|
238
|
-
const { api, json } = createNavigationApi(this, filePath);
|
|
239
|
-
const data = await api.listFolders();
|
|
240
|
-
const resolved = resolveFolder(data, folder);
|
|
241
|
-
|
|
242
|
-
if (json) {
|
|
243
|
-
emitJson(stdout, "navigation folders get", resolved, filePath);
|
|
244
|
-
return;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
printTable(folderRows([resolved]), ["id", "order", "name", "resourceCount"], stdout);
|
|
248
|
-
});
|
|
249
|
-
|
|
250
|
-
folders
|
|
251
|
-
.command("create")
|
|
252
|
-
.description("Create a navigation folder")
|
|
253
|
-
.requiredOption("--name <name>", "folder name")
|
|
254
|
-
.option("--order <n>", "display order (lower values appear first)")
|
|
255
|
-
.option("--props <json>", "additionalProperties JSON: free-form client metadata, e.g. {\"color\":\"blue\",\"icon\":\"chart-line\"}")
|
|
256
|
-
.option("--props-file <path>", "path to a file with the additionalProperties JSON")
|
|
257
|
-
.action(async function action(this: Command, commandOptions: FolderMutationOptions) {
|
|
258
|
-
const { api, json, readOnly } = createNavigationApi(this, filePath);
|
|
259
|
-
ensureWritable("navigation folders create", readOnly, "write");
|
|
260
|
-
const data = await api.createFolder(buildFolderBody(commandOptions));
|
|
261
|
-
|
|
262
|
-
if (json) {
|
|
263
|
-
emitJson(stdout, "navigation folders create", data, filePath);
|
|
264
|
-
return;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
writeLine(stdout, `Created navigation folder "${String(data.name ?? commandOptions.name)}".`);
|
|
268
|
-
});
|
|
269
|
-
|
|
270
|
-
folders
|
|
271
|
-
.command("update")
|
|
272
|
-
.description("Update a navigation folder")
|
|
273
|
-
.argument("<folder>", "folder UUID or exact name", parseUuidOrSlugSelector)
|
|
274
|
-
.option("--name <name>", "folder name")
|
|
275
|
-
.option("--order <n>", "display order (lower values appear first)")
|
|
276
|
-
.option("--props <json>", "additionalProperties JSON: free-form client metadata, e.g. {\"color\":\"blue\",\"icon\":\"chart-line\"}")
|
|
277
|
-
.option("--props-file <path>", "path to a file with the additionalProperties JSON")
|
|
278
|
-
.action(async function action(this: Command, folder: NormalizedUuidOrSlug, commandOptions: FolderMutationOptions) {
|
|
279
|
-
const { api, json, readOnly } = createNavigationApi(this, filePath);
|
|
280
|
-
ensureWritable("navigation folders update", readOnly, "write");
|
|
281
|
-
const allFolders = await api.listFolders();
|
|
282
|
-
const resolved = resolveFolder(allFolders, folder);
|
|
283
|
-
const body = buildFolderBody(commandOptions);
|
|
284
|
-
|
|
285
|
-
if (Object.keys(body).length === 0) {
|
|
286
|
-
throw new CliError("invalid_cli_usage", "Nothing to update.");
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
const data = await api.updateFolder({ kind: "uuid", value: resolved.id }, body);
|
|
290
|
-
|
|
291
|
-
if (json) {
|
|
292
|
-
emitJson(stdout, "navigation folders update", data, filePath);
|
|
293
|
-
return;
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
writeLine(stdout, `Updated navigation folder "${String(data.name ?? resolved.name ?? resolved.id)}".`);
|
|
297
|
-
});
|
|
298
|
-
|
|
299
|
-
folders
|
|
300
|
-
.command("delete")
|
|
301
|
-
.description("Delete a navigation folder")
|
|
302
|
-
.argument("<folder>", "folder UUID or exact name", parseUuidOrSlugSelector)
|
|
303
|
-
.action(async function action(this: Command, folder: NormalizedUuidOrSlug) {
|
|
304
|
-
const { api, json, readOnly } = createNavigationApi(this, filePath);
|
|
305
|
-
ensureWritable("navigation folders delete", readOnly, "destructive");
|
|
306
|
-
const allFolders = await api.listFolders();
|
|
307
|
-
const resolved = resolveFolder(allFolders, folder);
|
|
308
|
-
const data = await api.deleteFolder({ kind: "uuid", value: resolved.id });
|
|
309
|
-
|
|
310
|
-
if (json) {
|
|
311
|
-
emitJson(stdout, "navigation folders delete", data, filePath);
|
|
312
|
-
return;
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
writeLine(stdout, `Deleted navigation folder "${resolved.name ?? resolved.id}".`);
|
|
316
|
-
});
|
|
317
|
-
|
|
318
|
-
navigation
|
|
319
|
-
.command("resources")
|
|
320
|
-
.description("Manage navigation resources")
|
|
321
|
-
.command("move")
|
|
322
|
-
.description(
|
|
323
|
-
"Move a schema into a navigation folder. Only schemas can be moved (the navigation menu holds databases); schedules and sub-schemas follow their parent schema. Use the root folder UUID 00000000-0000-0000-0000-000000000000 to move it back to the workspace root.",
|
|
324
|
-
)
|
|
325
|
-
.argument("<resource>", "schema UUID")
|
|
326
|
-
.requiredOption("--to <folder>", "target folder UUID or exact name")
|
|
327
|
-
.action(async function action(
|
|
328
|
-
this: Command,
|
|
329
|
-
resourceId: string,
|
|
330
|
-
commandOptions: { to: string },
|
|
331
|
-
) {
|
|
332
|
-
const { api, json, readOnly } = createNavigationApi(this, filePath);
|
|
333
|
-
ensureWritable("navigation resources move", readOnly, "write");
|
|
334
|
-
const allFolders = await api.listFolders();
|
|
335
|
-
const resolved = resolveFolder(allFolders, parseUuidOrSlugSelector(commandOptions.to));
|
|
336
|
-
const data = await api.moveResource({
|
|
337
|
-
resourceId,
|
|
338
|
-
targetFolderId: resolved.id,
|
|
339
|
-
});
|
|
340
|
-
|
|
341
|
-
if (json) {
|
|
342
|
-
emitJson(stdout, "navigation resources move", data, filePath);
|
|
343
|
-
return;
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
writeLine(stdout, `Moved resource "${resourceId}" to "${resolved.name ?? resolved.id}".`);
|
|
347
|
-
});
|
|
348
|
-
}
|