ticktick-cli 0.1.1 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -4
- package/dist/auth.js +36 -12
- package/dist/bin.js +3 -0
- package/dist/cli.js +144 -108
- package/dist/config.js +10 -7
- package/dist/utils.js +7 -7
- package/package.json +10 -6
package/README.md
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
# TickTick CLI
|
|
1
|
+
# TickTick CLI v1
|
|
2
2
|
|
|
3
|
-
A
|
|
3
|
+
A TypeScript CLI wrapper for the TickTick Open API documented at:
|
|
4
4
|
|
|
5
5
|
- https://developer.ticktick.com/
|
|
6
6
|
- https://developer.ticktick.com/docs#/openapi
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
`v1.0.0` covers the documented OAuth flow plus every documented task and project endpoint.
|
|
9
|
+
|
|
10
|
+
The CLI is available as both `ticktick` and the short alias `tt`.
|
|
9
11
|
|
|
10
12
|
## What it covers
|
|
11
13
|
|
|
@@ -19,6 +21,10 @@ This wrapper covers the documented OAuth flow plus every documented task and pro
|
|
|
19
21
|
|
|
20
22
|
## Install
|
|
21
23
|
|
|
24
|
+
Requires Node.js `18+`.
|
|
25
|
+
|
|
26
|
+
Install from the local repo:
|
|
27
|
+
|
|
22
28
|
```bash
|
|
23
29
|
npm install
|
|
24
30
|
npm run build
|
|
@@ -27,7 +33,7 @@ npm run build
|
|
|
27
33
|
Run locally with:
|
|
28
34
|
|
|
29
35
|
```bash
|
|
30
|
-
node dist/
|
|
36
|
+
node dist/bin.js --help
|
|
31
37
|
```
|
|
32
38
|
|
|
33
39
|
Or install the built CLI globally from this directory:
|
|
@@ -35,8 +41,19 @@ Or install the built CLI globally from this directory:
|
|
|
35
41
|
```bash
|
|
36
42
|
npm install -g .
|
|
37
43
|
ticktick --help
|
|
44
|
+
tt --help
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Once published to npm, install it with:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
npm install -g ticktick-cli
|
|
51
|
+
ticktick --help
|
|
52
|
+
tt --help
|
|
38
53
|
```
|
|
39
54
|
|
|
55
|
+
All command examples below use `ticktick`, but `tt` works the same way.
|
|
56
|
+
|
|
40
57
|
## Configure
|
|
41
58
|
|
|
42
59
|
The CLI reads config in this order:
|
|
@@ -60,6 +77,12 @@ TICKTICK_AUTH_BASE_URL=https://ticktick.com
|
|
|
60
77
|
TICKTICK_CONFIG_FILE=/custom/path/config.json
|
|
61
78
|
```
|
|
62
79
|
|
|
80
|
+
Default local redirect URI:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
http://127.0.0.1:18463/callback
|
|
84
|
+
```
|
|
85
|
+
|
|
63
86
|
You can also persist config values:
|
|
64
87
|
|
|
65
88
|
```bash
|
|
@@ -180,6 +203,22 @@ Send a raw request to a full URL without bearer auth:
|
|
|
180
203
|
ticktick request POST https://httpbin.org/post --no-auth --json '{"hello":"world"}'
|
|
181
204
|
```
|
|
182
205
|
|
|
206
|
+
## Development
|
|
207
|
+
|
|
208
|
+
Run the normal test suite:
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
npm test
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
Run the enforced coverage check:
|
|
215
|
+
|
|
216
|
+
```bash
|
|
217
|
+
npm run coverage
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
`npm run coverage` currently enforces `100%` line, branch, and function coverage on the published runtime files.
|
|
221
|
+
|
|
183
222
|
## Notes
|
|
184
223
|
|
|
185
224
|
- The docs currently show `api.ticktick.com` for most endpoints, but `api.dida365.com` in the examples for `task/move`, `task/completed`, and `task/filter`. This CLI defaults to the selected service profile and lets you override base URLs explicitly if your account needs something different.
|
package/dist/auth.js
CHANGED
|
@@ -32,7 +32,16 @@ export async function exchangeAuthorizationCode(config, code, fetchImpl = fetch)
|
|
|
32
32
|
body: params,
|
|
33
33
|
});
|
|
34
34
|
const raw = await response.text();
|
|
35
|
-
|
|
35
|
+
let parsed = {};
|
|
36
|
+
if (raw.length > 0) {
|
|
37
|
+
try {
|
|
38
|
+
parsed = JSON.parse(raw);
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
42
|
+
throw new Error(`Token exchange returned invalid JSON: ${message}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
36
45
|
if (!response.ok) {
|
|
37
46
|
throw new Error(`Token exchange failed with status ${response.status}: ${JSON.stringify(parsed)}`);
|
|
38
47
|
}
|
|
@@ -46,18 +55,24 @@ export function isLoopbackRedirect(redirectUri) {
|
|
|
46
55
|
return (parsed.protocol === "http:" &&
|
|
47
56
|
["127.0.0.1", "localhost"].includes(parsed.hostname));
|
|
48
57
|
}
|
|
58
|
+
export function getOAuthCallbackBinding(redirectUri) {
|
|
59
|
+
const redirect = new URL(redirectUri);
|
|
60
|
+
return {
|
|
61
|
+
hostname: redirect.hostname === "localhost" ? "127.0.0.1" : redirect.hostname,
|
|
62
|
+
port: Number.parseInt(redirect.port || "80", 10),
|
|
63
|
+
callbackPath: redirect.pathname,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
49
66
|
export async function waitForOAuthCode(redirectUri, expectedState, timeoutMs = 120_000) {
|
|
50
67
|
const redirect = new URL(redirectUri);
|
|
51
|
-
const
|
|
52
|
-
const port = Number.parseInt(redirect.port || "80", 10);
|
|
53
|
-
const callbackPath = redirect.pathname || "/";
|
|
68
|
+
const { hostname, port, callbackPath } = getOAuthCallbackBinding(redirectUri);
|
|
54
69
|
return new Promise((resolve, reject) => {
|
|
55
70
|
const timeout = setTimeout(() => {
|
|
56
71
|
server.close();
|
|
57
72
|
reject(new Error("Timed out waiting for the OAuth callback."));
|
|
58
73
|
}, timeoutMs);
|
|
59
74
|
const server = createServer((request, response) => {
|
|
60
|
-
const requestUrl = new URL(request.url
|
|
75
|
+
const requestUrl = new URL(request.url, redirect.origin);
|
|
61
76
|
if (requestUrl.pathname !== callbackPath) {
|
|
62
77
|
response.statusCode = 404;
|
|
63
78
|
response.end("Not Found");
|
|
@@ -91,20 +106,29 @@ export async function waitForOAuthCode(redirectUri, expectedState, timeoutMs = 1
|
|
|
91
106
|
server.listen(port, hostname);
|
|
92
107
|
});
|
|
93
108
|
}
|
|
94
|
-
export async function openBrowser(url) {
|
|
109
|
+
export async function openBrowser(url, options) {
|
|
110
|
+
return openBrowserWith(url, options);
|
|
111
|
+
}
|
|
112
|
+
export async function openBrowserWith(url, options = {}) {
|
|
113
|
+
const platform = options.platform ?? process.platform;
|
|
114
|
+
const run = options.execFile ??
|
|
115
|
+
(async (file, args) => {
|
|
116
|
+
await execFileAsync(file, args);
|
|
117
|
+
});
|
|
118
|
+
const stderr = options.stderr ?? ((text) => process.stderr.write(text));
|
|
95
119
|
try {
|
|
96
|
-
if (
|
|
97
|
-
await
|
|
120
|
+
if (platform === "win32") {
|
|
121
|
+
await run("cmd", ["/c", "start", "", url]);
|
|
98
122
|
return;
|
|
99
123
|
}
|
|
100
|
-
if (
|
|
101
|
-
await
|
|
124
|
+
if (platform === "darwin") {
|
|
125
|
+
await run("open", [url]);
|
|
102
126
|
return;
|
|
103
127
|
}
|
|
104
|
-
await
|
|
128
|
+
await run("xdg-open", [url]);
|
|
105
129
|
}
|
|
106
130
|
catch (error) {
|
|
107
131
|
const message = error instanceof Error ? error.message : String(error);
|
|
108
|
-
|
|
132
|
+
stderr(`Could not open a browser automatically: ${message}\n`);
|
|
109
133
|
}
|
|
110
134
|
}
|
package/dist/bin.js
ADDED
package/dist/cli.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
1
|
import { Command } from "commander";
|
|
3
2
|
import { buildAuthorizationUrl, exchangeAuthorizationCode, isLoopbackRedirect, openBrowser, waitForOAuthCode, } from "./auth.js";
|
|
4
3
|
import { TickTickClient } from "./client.js";
|
|
@@ -14,26 +13,59 @@ const CONFIG_KEYS = new Set([
|
|
|
14
13
|
"apiBaseUrl",
|
|
15
14
|
"authBaseUrl",
|
|
16
15
|
]);
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
program
|
|
36
|
-
|
|
16
|
+
export const defaultCliDependencies = {
|
|
17
|
+
buildAuthorizationUrl,
|
|
18
|
+
exchangeAuthorizationCode,
|
|
19
|
+
isLoopbackRedirect,
|
|
20
|
+
loadJsonValue,
|
|
21
|
+
loadStoredConfig,
|
|
22
|
+
maskSecret,
|
|
23
|
+
mergeDefined,
|
|
24
|
+
openBrowser,
|
|
25
|
+
printJson,
|
|
26
|
+
resolveRuntimeConfig,
|
|
27
|
+
saveStoredConfig,
|
|
28
|
+
validateService,
|
|
29
|
+
waitForOAuthCode,
|
|
30
|
+
createClient: (config) => new TickTickClient(config),
|
|
31
|
+
stderr: (text) => process.stderr.write(text),
|
|
32
|
+
};
|
|
33
|
+
export function createProgram(dependencies = defaultCliDependencies) {
|
|
34
|
+
const program = new Command();
|
|
35
|
+
program
|
|
36
|
+
.name("ticktick")
|
|
37
|
+
.description("CLI wrapper for the TickTick Open API")
|
|
38
|
+
.option("--config-file <path>", "Custom config file path")
|
|
39
|
+
.option("--service <service>", 'Service: "ticktick" or "dida365"')
|
|
40
|
+
.option("--api-base-url <url>", "Override the API base URL")
|
|
41
|
+
.option("--auth-base-url <url>", "Override the OAuth base URL")
|
|
42
|
+
.option("--access-token <token>", "Override the access token for a single command")
|
|
43
|
+
.option("--client-id <id>", "Override the OAuth client id")
|
|
44
|
+
.option("--client-secret <secret>", "Override the OAuth client secret")
|
|
45
|
+
.option("--redirect-uri <uri>", "Override the OAuth redirect URI")
|
|
46
|
+
.option("--scopes <scopes>", "Override the OAuth scopes");
|
|
47
|
+
buildAuthCommands(program, dependencies);
|
|
48
|
+
buildConfigCommands(program, dependencies);
|
|
49
|
+
buildTaskCommands(program, dependencies);
|
|
50
|
+
buildProjectCommands(program, dependencies);
|
|
51
|
+
buildRequestCommand(program, dependencies);
|
|
52
|
+
return program;
|
|
53
|
+
}
|
|
54
|
+
export async function main(argv = process.argv, dependencies = defaultCliDependencies) {
|
|
55
|
+
const program = createProgram(dependencies);
|
|
56
|
+
try {
|
|
57
|
+
await program.parseAsync(argv);
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
handleError(error, dependencies);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
export function handleError(error, dependencies = defaultCliDependencies) {
|
|
64
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
65
|
+
dependencies.stderr(`${message}\n`);
|
|
66
|
+
process.exitCode = 1;
|
|
67
|
+
}
|
|
68
|
+
function buildAuthCommands(root, dependencies) {
|
|
37
69
|
const auth = root.command("auth").description("OAuth and token management");
|
|
38
70
|
auth
|
|
39
71
|
.command("url")
|
|
@@ -42,10 +74,9 @@ function buildAuthCommands(root) {
|
|
|
42
74
|
.action(async (...args) => {
|
|
43
75
|
const command = args.at(-1);
|
|
44
76
|
const options = command.optsWithGlobals();
|
|
45
|
-
const config = await resolveRuntimeConfig(runtimeOverrides(options));
|
|
77
|
+
const config = await dependencies.resolveRuntimeConfig(runtimeOverrides(options, dependencies));
|
|
46
78
|
requireClientId(config);
|
|
47
|
-
|
|
48
|
-
printJson(result);
|
|
79
|
+
dependencies.printJson(dependencies.buildAuthorizationUrl(config, options.state));
|
|
49
80
|
});
|
|
50
81
|
auth
|
|
51
82
|
.command("login")
|
|
@@ -54,23 +85,23 @@ function buildAuthCommands(root) {
|
|
|
54
85
|
.action(async (...args) => {
|
|
55
86
|
const command = args.at(-1);
|
|
56
87
|
const options = command.optsWithGlobals();
|
|
57
|
-
const config = await resolveRuntimeConfig(runtimeOverrides(options));
|
|
88
|
+
const config = await dependencies.resolveRuntimeConfig(runtimeOverrides(options, dependencies));
|
|
58
89
|
requireClientCredentials(config);
|
|
59
|
-
const { url, state } = buildAuthorizationUrl(config);
|
|
60
|
-
|
|
61
|
-
if (!isLoopbackRedirect(config.redirectUri)) {
|
|
62
|
-
printJson({
|
|
90
|
+
const { url, state } = dependencies.buildAuthorizationUrl(config);
|
|
91
|
+
dependencies.stderr(`Authorize URL:\n${url}\n`);
|
|
92
|
+
if (!dependencies.isLoopbackRedirect(config.redirectUri)) {
|
|
93
|
+
dependencies.printJson({
|
|
63
94
|
ok: false,
|
|
64
95
|
reason: "redirect_uri is not a local HTTP callback. Open the URL above and then run `ticktick auth exchange <code>`.",
|
|
65
96
|
});
|
|
66
97
|
return;
|
|
67
98
|
}
|
|
68
|
-
const codePromise = waitForOAuthCode(config.redirectUri, state, options.timeoutMs);
|
|
69
|
-
await openBrowser(url);
|
|
99
|
+
const codePromise = dependencies.waitForOAuthCode(config.redirectUri, state, options.timeoutMs);
|
|
100
|
+
await dependencies.openBrowser(url);
|
|
70
101
|
const code = await codePromise;
|
|
71
|
-
const token = await exchangeAuthorizationCode(config, code);
|
|
72
|
-
await persistConfig(config, token
|
|
73
|
-
printJson(token);
|
|
102
|
+
const token = await dependencies.exchangeAuthorizationCode(config, code);
|
|
103
|
+
await persistConfig(config, token, dependencies);
|
|
104
|
+
dependencies.printJson(token);
|
|
74
105
|
});
|
|
75
106
|
auth
|
|
76
107
|
.command("exchange <code>")
|
|
@@ -78,11 +109,11 @@ function buildAuthCommands(root) {
|
|
|
78
109
|
.action(async (...args) => {
|
|
79
110
|
const command = args.at(-1);
|
|
80
111
|
const [code] = args;
|
|
81
|
-
const config = await resolveRuntimeConfig(runtimeOverrides(command.optsWithGlobals()));
|
|
112
|
+
const config = await dependencies.resolveRuntimeConfig(runtimeOverrides(command.optsWithGlobals(), dependencies));
|
|
82
113
|
requireClientCredentials(config);
|
|
83
|
-
const token = await exchangeAuthorizationCode(config, code);
|
|
84
|
-
await persistConfig(config, token
|
|
85
|
-
printJson(token);
|
|
114
|
+
const token = await dependencies.exchangeAuthorizationCode(config, code);
|
|
115
|
+
await persistConfig(config, token, dependencies);
|
|
116
|
+
dependencies.printJson(token);
|
|
86
117
|
});
|
|
87
118
|
auth
|
|
88
119
|
.command("status")
|
|
@@ -91,15 +122,19 @@ function buildAuthCommands(root) {
|
|
|
91
122
|
.action(async (...args) => {
|
|
92
123
|
const command = args.at(-1);
|
|
93
124
|
const options = command.optsWithGlobals();
|
|
94
|
-
const config = await resolveRuntimeConfig(runtimeOverrides(options));
|
|
95
|
-
printJson({
|
|
125
|
+
const config = await dependencies.resolveRuntimeConfig(runtimeOverrides(options, dependencies));
|
|
126
|
+
dependencies.printJson({
|
|
96
127
|
service: config.service,
|
|
97
128
|
configFile: config.configFile,
|
|
98
129
|
clientId: config.clientId,
|
|
99
|
-
clientSecret: options.showSecrets
|
|
130
|
+
clientSecret: options.showSecrets
|
|
131
|
+
? config.clientSecret
|
|
132
|
+
: dependencies.maskSecret(config.clientSecret),
|
|
100
133
|
redirectUri: config.redirectUri,
|
|
101
134
|
scopes: config.scopes,
|
|
102
|
-
accessToken: options.showSecrets
|
|
135
|
+
accessToken: options.showSecrets
|
|
136
|
+
? config.accessToken
|
|
137
|
+
: dependencies.maskSecret(config.accessToken),
|
|
103
138
|
apiBaseUrl: config.apiBaseUrl,
|
|
104
139
|
authBaseUrl: config.authBaseUrl,
|
|
105
140
|
});
|
|
@@ -109,14 +144,14 @@ function buildAuthCommands(root) {
|
|
|
109
144
|
.description("Remove the stored access token from the config file")
|
|
110
145
|
.action(async (...args) => {
|
|
111
146
|
const command = args.at(-1);
|
|
112
|
-
const config = await resolveRuntimeConfig(runtimeOverrides(command.optsWithGlobals()));
|
|
113
|
-
const stored = await loadStoredConfig(config.configFile);
|
|
147
|
+
const config = await dependencies.resolveRuntimeConfig(runtimeOverrides(command.optsWithGlobals(), dependencies));
|
|
148
|
+
const stored = await dependencies.loadStoredConfig(config.configFile);
|
|
114
149
|
delete stored.accessToken;
|
|
115
|
-
await saveStoredConfig(config.configFile, stored);
|
|
116
|
-
printJson({ ok: true });
|
|
150
|
+
await dependencies.saveStoredConfig(config.configFile, stored);
|
|
151
|
+
dependencies.printJson({ ok: true });
|
|
117
152
|
});
|
|
118
153
|
}
|
|
119
|
-
function buildConfigCommands(root) {
|
|
154
|
+
function buildConfigCommands(root, dependencies) {
|
|
120
155
|
const config = root.command("config").description("Read and write local CLI config");
|
|
121
156
|
config
|
|
122
157
|
.command("show")
|
|
@@ -125,15 +160,19 @@ function buildConfigCommands(root) {
|
|
|
125
160
|
.action(async (...args) => {
|
|
126
161
|
const command = args.at(-1);
|
|
127
162
|
const options = command.optsWithGlobals();
|
|
128
|
-
const resolved = await resolveRuntimeConfig(runtimeOverrides(options));
|
|
129
|
-
printJson({
|
|
163
|
+
const resolved = await dependencies.resolveRuntimeConfig(runtimeOverrides(options, dependencies));
|
|
164
|
+
dependencies.printJson({
|
|
130
165
|
service: resolved.service,
|
|
131
166
|
configFile: resolved.configFile,
|
|
132
167
|
clientId: resolved.clientId,
|
|
133
|
-
clientSecret: options.showSecrets
|
|
168
|
+
clientSecret: options.showSecrets
|
|
169
|
+
? resolved.clientSecret
|
|
170
|
+
: dependencies.maskSecret(resolved.clientSecret),
|
|
134
171
|
redirectUri: resolved.redirectUri,
|
|
135
172
|
scopes: resolved.scopes,
|
|
136
|
-
accessToken: options.showSecrets
|
|
173
|
+
accessToken: options.showSecrets
|
|
174
|
+
? resolved.accessToken
|
|
175
|
+
: dependencies.maskSecret(resolved.accessToken),
|
|
137
176
|
apiBaseUrl: resolved.apiBaseUrl,
|
|
138
177
|
authBaseUrl: resolved.authBaseUrl,
|
|
139
178
|
});
|
|
@@ -144,15 +183,15 @@ function buildConfigCommands(root) {
|
|
|
144
183
|
.action(async (...args) => {
|
|
145
184
|
const command = args.at(-1);
|
|
146
185
|
const [key, value] = args;
|
|
147
|
-
const runtime = await resolveRuntimeConfig(runtimeOverrides(command.optsWithGlobals()));
|
|
186
|
+
const runtime = await dependencies.resolveRuntimeConfig(runtimeOverrides(command.optsWithGlobals(), dependencies));
|
|
148
187
|
if (!CONFIG_KEYS.has(key)) {
|
|
149
188
|
throw new Error(`Unsupported config key "${key}". Allowed keys: ${Array.from(CONFIG_KEYS).join(", ")}`);
|
|
150
189
|
}
|
|
151
|
-
const stored = await loadStoredConfig(runtime.configFile);
|
|
152
|
-
const normalizedValue = key === "service" ? validateService(value) : value;
|
|
190
|
+
const stored = await dependencies.loadStoredConfig(runtime.configFile);
|
|
191
|
+
const normalizedValue = key === "service" ? dependencies.validateService(value) : value;
|
|
153
192
|
const next = { ...stored, [key]: normalizedValue };
|
|
154
|
-
await saveStoredConfig(runtime.configFile, next);
|
|
155
|
-
printJson(next);
|
|
193
|
+
await dependencies.saveStoredConfig(runtime.configFile, next);
|
|
194
|
+
dependencies.printJson(next);
|
|
156
195
|
});
|
|
157
196
|
config
|
|
158
197
|
.command("unset <key>")
|
|
@@ -160,24 +199,24 @@ function buildConfigCommands(root) {
|
|
|
160
199
|
.action(async (...args) => {
|
|
161
200
|
const command = args.at(-1);
|
|
162
201
|
const [key] = args;
|
|
163
|
-
const runtime = await resolveRuntimeConfig(runtimeOverrides(command.optsWithGlobals()));
|
|
202
|
+
const runtime = await dependencies.resolveRuntimeConfig(runtimeOverrides(command.optsWithGlobals(), dependencies));
|
|
164
203
|
if (!CONFIG_KEYS.has(key)) {
|
|
165
204
|
throw new Error(`Unsupported config key "${key}". Allowed keys: ${Array.from(CONFIG_KEYS).join(", ")}`);
|
|
166
205
|
}
|
|
167
|
-
const stored = await loadStoredConfig(runtime.configFile);
|
|
206
|
+
const stored = await dependencies.loadStoredConfig(runtime.configFile);
|
|
168
207
|
delete stored[key];
|
|
169
|
-
await saveStoredConfig(runtime.configFile, stored);
|
|
170
|
-
printJson(stored);
|
|
208
|
+
await dependencies.saveStoredConfig(runtime.configFile, stored);
|
|
209
|
+
dependencies.printJson(stored);
|
|
171
210
|
});
|
|
172
211
|
}
|
|
173
|
-
function buildTaskCommands(root) {
|
|
212
|
+
function buildTaskCommands(root, dependencies) {
|
|
174
213
|
const task = root.command("task").description("Task endpoints");
|
|
175
214
|
task
|
|
176
215
|
.command("get <projectId> <taskId>")
|
|
177
216
|
.description("Get a task by project id and task id")
|
|
178
217
|
.action(async (...args) => {
|
|
179
218
|
const [projectId, taskId] = args;
|
|
180
|
-
await runClientCommand(args, (client) => client.getTask(projectId, taskId));
|
|
219
|
+
await runClientCommand(args, dependencies, (client) => client.getTask(projectId, taskId));
|
|
181
220
|
});
|
|
182
221
|
withJsonBody(task
|
|
183
222
|
.command("create")
|
|
@@ -207,11 +246,11 @@ function buildTaskCommands(root) {
|
|
|
207
246
|
priority: options.priority,
|
|
208
247
|
sortOrder: options.sortOrder,
|
|
209
248
|
isAllDay: options.allDay,
|
|
210
|
-
}));
|
|
249
|
+
}, dependencies));
|
|
211
250
|
if (!payload.projectId || !payload.title) {
|
|
212
251
|
throw new Error("Task creation requires both projectId and title.");
|
|
213
252
|
}
|
|
214
|
-
await runClientCommand(args, (client) => client.createTask(payload));
|
|
253
|
+
await runClientCommand(args, dependencies, (client) => client.createTask(payload));
|
|
215
254
|
});
|
|
216
255
|
withJsonBody(task
|
|
217
256
|
.command("update <taskId>")
|
|
@@ -243,26 +282,26 @@ function buildTaskCommands(root) {
|
|
|
243
282
|
priority: options.priority,
|
|
244
283
|
sortOrder: options.sortOrder,
|
|
245
284
|
isAllDay: options.allDay,
|
|
246
|
-
}));
|
|
285
|
+
}, dependencies));
|
|
247
286
|
payload.id = taskId;
|
|
248
287
|
if (!payload.projectId) {
|
|
249
288
|
throw new Error("Task update requires projectId.");
|
|
250
289
|
}
|
|
251
|
-
await runClientCommand(args, (client) => client.updateTask(taskId, payload));
|
|
290
|
+
await runClientCommand(args, dependencies, (client) => client.updateTask(taskId, payload));
|
|
252
291
|
});
|
|
253
292
|
task
|
|
254
293
|
.command("complete <projectId> <taskId>")
|
|
255
294
|
.description("Complete a task")
|
|
256
295
|
.action(async (...args) => {
|
|
257
296
|
const [projectId, taskId] = args;
|
|
258
|
-
await runClientCommand(args, (client) => client.completeTask(projectId, taskId));
|
|
297
|
+
await runClientCommand(args, dependencies, (client) => client.completeTask(projectId, taskId));
|
|
259
298
|
});
|
|
260
299
|
task
|
|
261
300
|
.command("delete <projectId> <taskId>")
|
|
262
301
|
.description("Delete a task")
|
|
263
302
|
.action(async (...args) => {
|
|
264
303
|
const [projectId, taskId] = args;
|
|
265
|
-
await runClientCommand(args, (client) => client.deleteTask(projectId, taskId));
|
|
304
|
+
await runClientCommand(args, dependencies, (client) => client.deleteTask(projectId, taskId));
|
|
266
305
|
});
|
|
267
306
|
withJsonBody(task
|
|
268
307
|
.command("move")
|
|
@@ -272,7 +311,7 @@ function buildTaskCommands(root) {
|
|
|
272
311
|
.option("--task-id <id>", "Task id")).action(async (...args) => {
|
|
273
312
|
const command = args.at(-1);
|
|
274
313
|
const options = command.optsWithGlobals();
|
|
275
|
-
let payload = (await loadJsonValue(options.json, options.jsonFile));
|
|
314
|
+
let payload = (await dependencies.loadJsonValue(options.json, options.jsonFile));
|
|
276
315
|
if (!payload) {
|
|
277
316
|
if (!options.fromProjectId || !options.toProjectId || !options.taskId) {
|
|
278
317
|
throw new Error("Provide --json/--json-file or all of --from-project-id, --to-project-id, and --task-id.");
|
|
@@ -288,7 +327,7 @@ function buildTaskCommands(root) {
|
|
|
288
327
|
if (!Array.isArray(payload)) {
|
|
289
328
|
throw new Error("Move payload must be a JSON array.");
|
|
290
329
|
}
|
|
291
|
-
await runClientCommand(args, (client) => client.moveTasks(payload));
|
|
330
|
+
await runClientCommand(args, dependencies, (client) => client.moveTasks(payload));
|
|
292
331
|
});
|
|
293
332
|
withJsonBody(task
|
|
294
333
|
.command("completed")
|
|
@@ -302,8 +341,8 @@ function buildTaskCommands(root) {
|
|
|
302
341
|
projectIds: options.projectId && options.projectId.length > 0 ? options.projectId : undefined,
|
|
303
342
|
startDate: options.startDate,
|
|
304
343
|
endDate: options.endDate,
|
|
305
|
-
}));
|
|
306
|
-
await runClientCommand(args, (client) => client.listCompletedTasks(payload));
|
|
344
|
+
}, dependencies));
|
|
345
|
+
await runClientCommand(args, dependencies, (client) => client.listCompletedTasks(payload));
|
|
307
346
|
});
|
|
308
347
|
withJsonBody(task
|
|
309
348
|
.command("filter")
|
|
@@ -323,31 +362,31 @@ function buildTaskCommands(root) {
|
|
|
323
362
|
priority: options.priority && options.priority.length > 0 ? options.priority : undefined,
|
|
324
363
|
tag: options.tag && options.tag.length > 0 ? options.tag : undefined,
|
|
325
364
|
status: options.status && options.status.length > 0 ? options.status : undefined,
|
|
326
|
-
}));
|
|
327
|
-
await runClientCommand(args, (client) => client.filterTasks(payload));
|
|
365
|
+
}, dependencies));
|
|
366
|
+
await runClientCommand(args, dependencies, (client) => client.filterTasks(payload));
|
|
328
367
|
});
|
|
329
368
|
}
|
|
330
|
-
function buildProjectCommands(root) {
|
|
369
|
+
function buildProjectCommands(root, dependencies) {
|
|
331
370
|
const project = root.command("project").description("Project endpoints");
|
|
332
371
|
project
|
|
333
372
|
.command("list")
|
|
334
373
|
.description("List user projects")
|
|
335
374
|
.action(async (...args) => {
|
|
336
|
-
await runClientCommand(args, (client) => client.listProjects());
|
|
375
|
+
await runClientCommand(args, dependencies, (client) => client.listProjects());
|
|
337
376
|
});
|
|
338
377
|
project
|
|
339
378
|
.command("get <projectId>")
|
|
340
379
|
.description("Get a project by id")
|
|
341
380
|
.action(async (...args) => {
|
|
342
381
|
const [projectId] = args;
|
|
343
|
-
await runClientCommand(args, (client) => client.getProject(projectId));
|
|
382
|
+
await runClientCommand(args, dependencies, (client) => client.getProject(projectId));
|
|
344
383
|
});
|
|
345
384
|
project
|
|
346
385
|
.command("data <projectId>")
|
|
347
386
|
.description("Get a project together with tasks and columns")
|
|
348
387
|
.action(async (...args) => {
|
|
349
388
|
const [projectId] = args;
|
|
350
|
-
await runClientCommand(args, (client) => client.getProjectData(projectId));
|
|
389
|
+
await runClientCommand(args, dependencies, (client) => client.getProjectData(projectId));
|
|
351
390
|
});
|
|
352
391
|
withJsonBody(project
|
|
353
392
|
.command("create")
|
|
@@ -365,11 +404,11 @@ function buildProjectCommands(root) {
|
|
|
365
404
|
sortOrder: options.sortOrder,
|
|
366
405
|
viewMode: options.viewMode,
|
|
367
406
|
kind: options.kind,
|
|
368
|
-
}));
|
|
407
|
+
}, dependencies));
|
|
369
408
|
if (!payload.name) {
|
|
370
409
|
throw new Error("Project creation requires name.");
|
|
371
410
|
}
|
|
372
|
-
await runClientCommand(args, (client) => client.createProject(payload));
|
|
411
|
+
await runClientCommand(args, dependencies, (client) => client.createProject(payload));
|
|
373
412
|
});
|
|
374
413
|
withJsonBody(project
|
|
375
414
|
.command("update <projectId>")
|
|
@@ -388,18 +427,18 @@ function buildProjectCommands(root) {
|
|
|
388
427
|
sortOrder: options.sortOrder,
|
|
389
428
|
viewMode: options.viewMode,
|
|
390
429
|
kind: options.kind,
|
|
391
|
-
}));
|
|
392
|
-
await runClientCommand(args, (client) => client.updateProject(projectId, payload));
|
|
430
|
+
}, dependencies));
|
|
431
|
+
await runClientCommand(args, dependencies, (client) => client.updateProject(projectId, payload));
|
|
393
432
|
});
|
|
394
433
|
project
|
|
395
434
|
.command("delete <projectId>")
|
|
396
435
|
.description("Delete a project")
|
|
397
436
|
.action(async (...args) => {
|
|
398
437
|
const [projectId] = args;
|
|
399
|
-
await runClientCommand(args, (client) => client.deleteProject(projectId));
|
|
438
|
+
await runClientCommand(args, dependencies, (client) => client.deleteProject(projectId));
|
|
400
439
|
});
|
|
401
440
|
}
|
|
402
|
-
function buildRequestCommand(root) {
|
|
441
|
+
function buildRequestCommand(root, dependencies) {
|
|
403
442
|
withJsonBody(root
|
|
404
443
|
.command("request <method> <path>")
|
|
405
444
|
.description("Send a raw request to the configured API base URL, or to a full URL")
|
|
@@ -407,11 +446,13 @@ function buildRequestCommand(root) {
|
|
|
407
446
|
const command = args.at(-1);
|
|
408
447
|
const [method, path] = args;
|
|
409
448
|
const options = command.optsWithGlobals();
|
|
410
|
-
const config = await resolveRuntimeConfig(runtimeOverrides(options));
|
|
411
|
-
const client =
|
|
412
|
-
const body = await loadJsonValue(options.json, options.jsonFile);
|
|
413
|
-
const result = await client.requestRaw(method.toUpperCase(), path, body, {
|
|
414
|
-
|
|
449
|
+
const config = await dependencies.resolveRuntimeConfig(runtimeOverrides(options, dependencies));
|
|
450
|
+
const client = dependencies.createClient(config);
|
|
451
|
+
const body = await dependencies.loadJsonValue(options.json, options.jsonFile);
|
|
452
|
+
const result = await client.requestRaw(method.toUpperCase(), path, body, {
|
|
453
|
+
auth: options.auth,
|
|
454
|
+
});
|
|
455
|
+
dependencies.printJson(result ?? { ok: true });
|
|
415
456
|
});
|
|
416
457
|
}
|
|
417
458
|
function withJsonBody(command) {
|
|
@@ -419,10 +460,10 @@ function withJsonBody(command) {
|
|
|
419
460
|
.option("--json <json>", "Inline JSON body")
|
|
420
461
|
.option("--json-file <path>", "Path to a JSON body file");
|
|
421
462
|
}
|
|
422
|
-
function runtimeOverrides(options) {
|
|
463
|
+
function runtimeOverrides(options, dependencies) {
|
|
423
464
|
return {
|
|
424
465
|
configFile: options.configFile,
|
|
425
|
-
service: options.service ? validateService(options.service) : undefined,
|
|
466
|
+
service: options.service ? dependencies.validateService(options.service) : undefined,
|
|
426
467
|
apiBaseUrl: options.apiBaseUrl,
|
|
427
468
|
authBaseUrl: options.authBaseUrl,
|
|
428
469
|
accessToken: options.accessToken,
|
|
@@ -432,28 +473,28 @@ function runtimeOverrides(options) {
|
|
|
432
473
|
scopes: options.scopes,
|
|
433
474
|
};
|
|
434
475
|
}
|
|
435
|
-
async function runClientCommand(args, runner) {
|
|
476
|
+
async function runClientCommand(args, dependencies, runner) {
|
|
436
477
|
const command = args.at(-1);
|
|
437
|
-
const config = await resolveRuntimeConfig(runtimeOverrides(command.optsWithGlobals()));
|
|
438
|
-
const client =
|
|
478
|
+
const config = await dependencies.resolveRuntimeConfig(runtimeOverrides(command.optsWithGlobals(), dependencies));
|
|
479
|
+
const client = dependencies.createClient(config);
|
|
439
480
|
const result = await runner(client, config);
|
|
440
|
-
printJson(result ?? { ok: true });
|
|
481
|
+
dependencies.printJson(result ?? { ok: true });
|
|
441
482
|
}
|
|
442
|
-
async function loadObjectPayload(options, flags) {
|
|
443
|
-
const loaded = await loadJsonValue(options.json, options.jsonFile);
|
|
483
|
+
async function loadObjectPayload(options, flags, dependencies) {
|
|
484
|
+
const loaded = await dependencies.loadJsonValue(options.json, options.jsonFile);
|
|
444
485
|
if (loaded === undefined) {
|
|
445
|
-
return mergeDefined({}, flags);
|
|
486
|
+
return dependencies.mergeDefined({}, flags);
|
|
446
487
|
}
|
|
447
488
|
if (!isPlainObject(loaded)) {
|
|
448
489
|
throw new Error("Expected a JSON object payload.");
|
|
449
490
|
}
|
|
450
|
-
return mergeDefined(loaded, flags);
|
|
491
|
+
return dependencies.mergeDefined(loaded, flags);
|
|
451
492
|
}
|
|
452
493
|
function isPlainObject(value) {
|
|
453
494
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
454
495
|
}
|
|
455
|
-
async function persistConfig(runtime,
|
|
456
|
-
const stored = await loadStoredConfig(runtime.configFile);
|
|
496
|
+
async function persistConfig(runtime, token, dependencies) {
|
|
497
|
+
const stored = await dependencies.loadStoredConfig(runtime.configFile);
|
|
457
498
|
const next = {
|
|
458
499
|
...stored,
|
|
459
500
|
service: runtime.service,
|
|
@@ -463,9 +504,9 @@ async function persistConfig(runtime, accessToken) {
|
|
|
463
504
|
scopes: runtime.scopes,
|
|
464
505
|
apiBaseUrl: runtime.apiBaseUrl,
|
|
465
506
|
authBaseUrl: runtime.authBaseUrl,
|
|
466
|
-
accessToken,
|
|
507
|
+
accessToken: token.access_token,
|
|
467
508
|
};
|
|
468
|
-
await saveStoredConfig(runtime.configFile, next);
|
|
509
|
+
await dependencies.saveStoredConfig(runtime.configFile, next);
|
|
469
510
|
}
|
|
470
511
|
function requireClientCredentials(config) {
|
|
471
512
|
if (!config.clientId || !config.clientSecret) {
|
|
@@ -483,8 +524,3 @@ function collectString(value, previous = []) {
|
|
|
483
524
|
function collectInteger(value, previous = []) {
|
|
484
525
|
return [...previous, parseInteger(value)];
|
|
485
526
|
}
|
|
486
|
-
function handleError(error) {
|
|
487
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
488
|
-
process.stderr.write(`${message}\n`);
|
|
489
|
-
process.exitCode = 1;
|
|
490
|
-
}
|
package/dist/config.js
CHANGED
|
@@ -13,15 +13,18 @@ const SERVICE_DEFAULTS = {
|
|
|
13
13
|
authBaseUrl: "https://dida365.com",
|
|
14
14
|
},
|
|
15
15
|
};
|
|
16
|
-
export function defaultConfigFilePath() {
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
export function defaultConfigFilePath(options = {}) {
|
|
17
|
+
const platform = options.platform ?? process.platform;
|
|
18
|
+
const appData = options.appData ?? process.env.APPDATA;
|
|
19
|
+
const homeDir = options.homeDir ?? os.homedir();
|
|
20
|
+
const pathModule = platform === "win32" ? path.win32 : path.posix;
|
|
21
|
+
if (platform === "win32" && appData) {
|
|
22
|
+
return pathModule.join(appData, "ticktick-cli", "config.json");
|
|
20
23
|
}
|
|
21
|
-
if (
|
|
22
|
-
return
|
|
24
|
+
if (platform === "darwin") {
|
|
25
|
+
return pathModule.join(homeDir, "Library", "Application Support", "ticktick-cli", "config.json");
|
|
23
26
|
}
|
|
24
|
-
return
|
|
27
|
+
return pathModule.join(homeDir, ".config", "ticktick-cli", "config.json");
|
|
25
28
|
}
|
|
26
29
|
export async function loadStoredConfig(configFile) {
|
|
27
30
|
try {
|
package/dist/utils.js
CHANGED
|
@@ -25,24 +25,24 @@ export function parseInteger(value) {
|
|
|
25
25
|
}
|
|
26
26
|
return parsed;
|
|
27
27
|
}
|
|
28
|
-
export async function maybeReadStdin() {
|
|
29
|
-
if (
|
|
28
|
+
export async function maybeReadStdin(input = process.stdin) {
|
|
29
|
+
if (input.isTTY) {
|
|
30
30
|
return undefined;
|
|
31
31
|
}
|
|
32
32
|
const chunks = [];
|
|
33
|
-
for await (const chunk of
|
|
33
|
+
for await (const chunk of input) {
|
|
34
34
|
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
35
35
|
}
|
|
36
36
|
const text = Buffer.concat(chunks).toString("utf8").trim();
|
|
37
37
|
return text.length > 0 ? text : undefined;
|
|
38
38
|
}
|
|
39
|
-
export async function loadJsonValue(inlineJson, jsonFile) {
|
|
39
|
+
export async function loadJsonValue(inlineJson, jsonFile, input = process.stdin) {
|
|
40
40
|
let raw = inlineJson;
|
|
41
41
|
if (jsonFile) {
|
|
42
42
|
raw = await readFile(jsonFile, "utf8");
|
|
43
43
|
}
|
|
44
44
|
if (!raw) {
|
|
45
|
-
raw = await maybeReadStdin();
|
|
45
|
+
raw = await maybeReadStdin(input);
|
|
46
46
|
}
|
|
47
47
|
if (!raw) {
|
|
48
48
|
return undefined;
|
|
@@ -55,8 +55,8 @@ export async function loadJsonValue(inlineJson, jsonFile) {
|
|
|
55
55
|
throw new Error(`Failed to parse JSON input: ${message}`);
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
|
-
export function printJson(value) {
|
|
59
|
-
|
|
58
|
+
export function printJson(value, write = (text) => process.stdout.write(text)) {
|
|
59
|
+
write(`${JSON.stringify(value, null, 2)}\n`);
|
|
60
60
|
}
|
|
61
61
|
export function maskSecret(value) {
|
|
62
62
|
if (!value) {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ticktick-cli",
|
|
3
|
-
"version": "0.1
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "CLI wrapper for the TickTick Open API",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
7
7
|
"dist",
|
|
@@ -9,15 +9,19 @@
|
|
|
9
9
|
"LICENSE"
|
|
10
10
|
],
|
|
11
11
|
"bin": {
|
|
12
|
-
"ticktick": "dist/
|
|
12
|
+
"ticktick": "dist/bin.js",
|
|
13
|
+
"tt": "dist/bin.js"
|
|
13
14
|
},
|
|
14
15
|
"scripts": {
|
|
15
16
|
"build": "npm run clean && tsc -p tsconfig.json",
|
|
16
|
-
"
|
|
17
|
+
"build:coverage": "npm run clean:coverage && tsc -p tsconfig.coverage.json",
|
|
18
|
+
"check": "npm run coverage",
|
|
17
19
|
"clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"",
|
|
20
|
+
"clean:coverage": "node -e \"require('node:fs').rmSync('.coverage-dist',{recursive:true,force:true})\"",
|
|
18
21
|
"dev": "tsx src/cli.ts",
|
|
19
|
-
"prepack": "npm run
|
|
20
|
-
"test": "npm run build && node --import tsx --test src/test/*.test.ts"
|
|
22
|
+
"prepack": "npm run coverage",
|
|
23
|
+
"test": "npm run build && node --import tsx --test src/test/*.test.ts",
|
|
24
|
+
"coverage": "npm run build:coverage && node --experimental-test-coverage --test-coverage-lines=100 --test-coverage-functions=100 --test-coverage-branches=100 --test-coverage-include=.coverage-dist/*.js --test-coverage-exclude=.coverage-dist/bin.js --test .coverage-dist/test/*.test.js"
|
|
21
25
|
},
|
|
22
26
|
"keywords": [
|
|
23
27
|
"ticktick",
|