ticktick-cli 0.1.1 → 1.0.2
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 +35 -4
- package/dist/auth.js +36 -12
- package/dist/bin.js +3 -0
- package/dist/cli.js +150 -107
- package/dist/config.js +10 -7
- package/dist/utils.js +7 -7
- package/package.json +5 -5
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.2` 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,14 @@ 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
|
+
Build the CLI:
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
npm run build
|
|
212
|
+
```
|
|
213
|
+
|
|
183
214
|
## Notes
|
|
184
215
|
|
|
185
216
|
- 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,5 +1,5 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
1
|
import { Command } from "commander";
|
|
2
|
+
import { pathToFileURL } from "node:url";
|
|
3
3
|
import { buildAuthorizationUrl, exchangeAuthorizationCode, isLoopbackRedirect, openBrowser, waitForOAuthCode, } from "./auth.js";
|
|
4
4
|
import { TickTickClient } from "./client.js";
|
|
5
5
|
import { loadStoredConfig, resolveRuntimeConfig, saveStoredConfig, validateService, } from "./config.js";
|
|
@@ -14,26 +14,59 @@ const CONFIG_KEYS = new Set([
|
|
|
14
14
|
"apiBaseUrl",
|
|
15
15
|
"authBaseUrl",
|
|
16
16
|
]);
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
program
|
|
36
|
-
|
|
17
|
+
export const defaultCliDependencies = {
|
|
18
|
+
buildAuthorizationUrl,
|
|
19
|
+
exchangeAuthorizationCode,
|
|
20
|
+
isLoopbackRedirect,
|
|
21
|
+
loadJsonValue,
|
|
22
|
+
loadStoredConfig,
|
|
23
|
+
maskSecret,
|
|
24
|
+
mergeDefined,
|
|
25
|
+
openBrowser,
|
|
26
|
+
printJson,
|
|
27
|
+
resolveRuntimeConfig,
|
|
28
|
+
saveStoredConfig,
|
|
29
|
+
validateService,
|
|
30
|
+
waitForOAuthCode,
|
|
31
|
+
createClient: (config) => new TickTickClient(config),
|
|
32
|
+
stderr: (text) => process.stderr.write(text),
|
|
33
|
+
};
|
|
34
|
+
export function createProgram(dependencies = defaultCliDependencies) {
|
|
35
|
+
const program = new Command();
|
|
36
|
+
program
|
|
37
|
+
.name("ticktick")
|
|
38
|
+
.description("CLI wrapper for the TickTick Open API")
|
|
39
|
+
.option("--config-file <path>", "Custom config file path")
|
|
40
|
+
.option("--service <service>", 'Service: "ticktick" or "dida365"')
|
|
41
|
+
.option("--api-base-url <url>", "Override the API base URL")
|
|
42
|
+
.option("--auth-base-url <url>", "Override the OAuth base URL")
|
|
43
|
+
.option("--access-token <token>", "Override the access token for a single command")
|
|
44
|
+
.option("--client-id <id>", "Override the OAuth client id")
|
|
45
|
+
.option("--client-secret <secret>", "Override the OAuth client secret")
|
|
46
|
+
.option("--redirect-uri <uri>", "Override the OAuth redirect URI")
|
|
47
|
+
.option("--scopes <scopes>", "Override the OAuth scopes");
|
|
48
|
+
buildAuthCommands(program, dependencies);
|
|
49
|
+
buildConfigCommands(program, dependencies);
|
|
50
|
+
buildTaskCommands(program, dependencies);
|
|
51
|
+
buildProjectCommands(program, dependencies);
|
|
52
|
+
buildRequestCommand(program, dependencies);
|
|
53
|
+
return program;
|
|
54
|
+
}
|
|
55
|
+
export async function main(argv = process.argv, dependencies = defaultCliDependencies) {
|
|
56
|
+
const program = createProgram(dependencies);
|
|
57
|
+
try {
|
|
58
|
+
await program.parseAsync(argv);
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
handleError(error, dependencies);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
export function handleError(error, dependencies = defaultCliDependencies) {
|
|
65
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
66
|
+
dependencies.stderr(`${message}\n`);
|
|
67
|
+
process.exitCode = 1;
|
|
68
|
+
}
|
|
69
|
+
function buildAuthCommands(root, dependencies) {
|
|
37
70
|
const auth = root.command("auth").description("OAuth and token management");
|
|
38
71
|
auth
|
|
39
72
|
.command("url")
|
|
@@ -42,10 +75,9 @@ function buildAuthCommands(root) {
|
|
|
42
75
|
.action(async (...args) => {
|
|
43
76
|
const command = args.at(-1);
|
|
44
77
|
const options = command.optsWithGlobals();
|
|
45
|
-
const config = await resolveRuntimeConfig(runtimeOverrides(options));
|
|
78
|
+
const config = await dependencies.resolveRuntimeConfig(runtimeOverrides(options, dependencies));
|
|
46
79
|
requireClientId(config);
|
|
47
|
-
|
|
48
|
-
printJson(result);
|
|
80
|
+
dependencies.printJson(dependencies.buildAuthorizationUrl(config, options.state));
|
|
49
81
|
});
|
|
50
82
|
auth
|
|
51
83
|
.command("login")
|
|
@@ -54,23 +86,23 @@ function buildAuthCommands(root) {
|
|
|
54
86
|
.action(async (...args) => {
|
|
55
87
|
const command = args.at(-1);
|
|
56
88
|
const options = command.optsWithGlobals();
|
|
57
|
-
const config = await resolveRuntimeConfig(runtimeOverrides(options));
|
|
89
|
+
const config = await dependencies.resolveRuntimeConfig(runtimeOverrides(options, dependencies));
|
|
58
90
|
requireClientCredentials(config);
|
|
59
|
-
const { url, state } = buildAuthorizationUrl(config);
|
|
60
|
-
|
|
61
|
-
if (!isLoopbackRedirect(config.redirectUri)) {
|
|
62
|
-
printJson({
|
|
91
|
+
const { url, state } = dependencies.buildAuthorizationUrl(config);
|
|
92
|
+
dependencies.stderr(`Authorize URL:\n${url}\n`);
|
|
93
|
+
if (!dependencies.isLoopbackRedirect(config.redirectUri)) {
|
|
94
|
+
dependencies.printJson({
|
|
63
95
|
ok: false,
|
|
64
96
|
reason: "redirect_uri is not a local HTTP callback. Open the URL above and then run `ticktick auth exchange <code>`.",
|
|
65
97
|
});
|
|
66
98
|
return;
|
|
67
99
|
}
|
|
68
|
-
const codePromise = waitForOAuthCode(config.redirectUri, state, options.timeoutMs);
|
|
69
|
-
await openBrowser(url);
|
|
100
|
+
const codePromise = dependencies.waitForOAuthCode(config.redirectUri, state, options.timeoutMs);
|
|
101
|
+
await dependencies.openBrowser(url);
|
|
70
102
|
const code = await codePromise;
|
|
71
|
-
const token = await exchangeAuthorizationCode(config, code);
|
|
72
|
-
await persistConfig(config, token
|
|
73
|
-
printJson(token);
|
|
103
|
+
const token = await dependencies.exchangeAuthorizationCode(config, code);
|
|
104
|
+
await persistConfig(config, token, dependencies);
|
|
105
|
+
dependencies.printJson(token);
|
|
74
106
|
});
|
|
75
107
|
auth
|
|
76
108
|
.command("exchange <code>")
|
|
@@ -78,11 +110,11 @@ function buildAuthCommands(root) {
|
|
|
78
110
|
.action(async (...args) => {
|
|
79
111
|
const command = args.at(-1);
|
|
80
112
|
const [code] = args;
|
|
81
|
-
const config = await resolveRuntimeConfig(runtimeOverrides(command.optsWithGlobals()));
|
|
113
|
+
const config = await dependencies.resolveRuntimeConfig(runtimeOverrides(command.optsWithGlobals(), dependencies));
|
|
82
114
|
requireClientCredentials(config);
|
|
83
|
-
const token = await exchangeAuthorizationCode(config, code);
|
|
84
|
-
await persistConfig(config, token
|
|
85
|
-
printJson(token);
|
|
115
|
+
const token = await dependencies.exchangeAuthorizationCode(config, code);
|
|
116
|
+
await persistConfig(config, token, dependencies);
|
|
117
|
+
dependencies.printJson(token);
|
|
86
118
|
});
|
|
87
119
|
auth
|
|
88
120
|
.command("status")
|
|
@@ -91,15 +123,19 @@ function buildAuthCommands(root) {
|
|
|
91
123
|
.action(async (...args) => {
|
|
92
124
|
const command = args.at(-1);
|
|
93
125
|
const options = command.optsWithGlobals();
|
|
94
|
-
const config = await resolveRuntimeConfig(runtimeOverrides(options));
|
|
95
|
-
printJson({
|
|
126
|
+
const config = await dependencies.resolveRuntimeConfig(runtimeOverrides(options, dependencies));
|
|
127
|
+
dependencies.printJson({
|
|
96
128
|
service: config.service,
|
|
97
129
|
configFile: config.configFile,
|
|
98
130
|
clientId: config.clientId,
|
|
99
|
-
clientSecret: options.showSecrets
|
|
131
|
+
clientSecret: options.showSecrets
|
|
132
|
+
? config.clientSecret
|
|
133
|
+
: dependencies.maskSecret(config.clientSecret),
|
|
100
134
|
redirectUri: config.redirectUri,
|
|
101
135
|
scopes: config.scopes,
|
|
102
|
-
accessToken: options.showSecrets
|
|
136
|
+
accessToken: options.showSecrets
|
|
137
|
+
? config.accessToken
|
|
138
|
+
: dependencies.maskSecret(config.accessToken),
|
|
103
139
|
apiBaseUrl: config.apiBaseUrl,
|
|
104
140
|
authBaseUrl: config.authBaseUrl,
|
|
105
141
|
});
|
|
@@ -109,14 +145,14 @@ function buildAuthCommands(root) {
|
|
|
109
145
|
.description("Remove the stored access token from the config file")
|
|
110
146
|
.action(async (...args) => {
|
|
111
147
|
const command = args.at(-1);
|
|
112
|
-
const config = await resolveRuntimeConfig(runtimeOverrides(command.optsWithGlobals()));
|
|
113
|
-
const stored = await loadStoredConfig(config.configFile);
|
|
148
|
+
const config = await dependencies.resolveRuntimeConfig(runtimeOverrides(command.optsWithGlobals(), dependencies));
|
|
149
|
+
const stored = await dependencies.loadStoredConfig(config.configFile);
|
|
114
150
|
delete stored.accessToken;
|
|
115
|
-
await saveStoredConfig(config.configFile, stored);
|
|
116
|
-
printJson({ ok: true });
|
|
151
|
+
await dependencies.saveStoredConfig(config.configFile, stored);
|
|
152
|
+
dependencies.printJson({ ok: true });
|
|
117
153
|
});
|
|
118
154
|
}
|
|
119
|
-
function buildConfigCommands(root) {
|
|
155
|
+
function buildConfigCommands(root, dependencies) {
|
|
120
156
|
const config = root.command("config").description("Read and write local CLI config");
|
|
121
157
|
config
|
|
122
158
|
.command("show")
|
|
@@ -125,15 +161,19 @@ function buildConfigCommands(root) {
|
|
|
125
161
|
.action(async (...args) => {
|
|
126
162
|
const command = args.at(-1);
|
|
127
163
|
const options = command.optsWithGlobals();
|
|
128
|
-
const resolved = await resolveRuntimeConfig(runtimeOverrides(options));
|
|
129
|
-
printJson({
|
|
164
|
+
const resolved = await dependencies.resolveRuntimeConfig(runtimeOverrides(options, dependencies));
|
|
165
|
+
dependencies.printJson({
|
|
130
166
|
service: resolved.service,
|
|
131
167
|
configFile: resolved.configFile,
|
|
132
168
|
clientId: resolved.clientId,
|
|
133
|
-
clientSecret: options.showSecrets
|
|
169
|
+
clientSecret: options.showSecrets
|
|
170
|
+
? resolved.clientSecret
|
|
171
|
+
: dependencies.maskSecret(resolved.clientSecret),
|
|
134
172
|
redirectUri: resolved.redirectUri,
|
|
135
173
|
scopes: resolved.scopes,
|
|
136
|
-
accessToken: options.showSecrets
|
|
174
|
+
accessToken: options.showSecrets
|
|
175
|
+
? resolved.accessToken
|
|
176
|
+
: dependencies.maskSecret(resolved.accessToken),
|
|
137
177
|
apiBaseUrl: resolved.apiBaseUrl,
|
|
138
178
|
authBaseUrl: resolved.authBaseUrl,
|
|
139
179
|
});
|
|
@@ -144,15 +184,15 @@ function buildConfigCommands(root) {
|
|
|
144
184
|
.action(async (...args) => {
|
|
145
185
|
const command = args.at(-1);
|
|
146
186
|
const [key, value] = args;
|
|
147
|
-
const runtime = await resolveRuntimeConfig(runtimeOverrides(command.optsWithGlobals()));
|
|
187
|
+
const runtime = await dependencies.resolveRuntimeConfig(runtimeOverrides(command.optsWithGlobals(), dependencies));
|
|
148
188
|
if (!CONFIG_KEYS.has(key)) {
|
|
149
189
|
throw new Error(`Unsupported config key "${key}". Allowed keys: ${Array.from(CONFIG_KEYS).join(", ")}`);
|
|
150
190
|
}
|
|
151
|
-
const stored = await loadStoredConfig(runtime.configFile);
|
|
152
|
-
const normalizedValue = key === "service" ? validateService(value) : value;
|
|
191
|
+
const stored = await dependencies.loadStoredConfig(runtime.configFile);
|
|
192
|
+
const normalizedValue = key === "service" ? dependencies.validateService(value) : value;
|
|
153
193
|
const next = { ...stored, [key]: normalizedValue };
|
|
154
|
-
await saveStoredConfig(runtime.configFile, next);
|
|
155
|
-
printJson(next);
|
|
194
|
+
await dependencies.saveStoredConfig(runtime.configFile, next);
|
|
195
|
+
dependencies.printJson(next);
|
|
156
196
|
});
|
|
157
197
|
config
|
|
158
198
|
.command("unset <key>")
|
|
@@ -160,24 +200,24 @@ function buildConfigCommands(root) {
|
|
|
160
200
|
.action(async (...args) => {
|
|
161
201
|
const command = args.at(-1);
|
|
162
202
|
const [key] = args;
|
|
163
|
-
const runtime = await resolveRuntimeConfig(runtimeOverrides(command.optsWithGlobals()));
|
|
203
|
+
const runtime = await dependencies.resolveRuntimeConfig(runtimeOverrides(command.optsWithGlobals(), dependencies));
|
|
164
204
|
if (!CONFIG_KEYS.has(key)) {
|
|
165
205
|
throw new Error(`Unsupported config key "${key}". Allowed keys: ${Array.from(CONFIG_KEYS).join(", ")}`);
|
|
166
206
|
}
|
|
167
|
-
const stored = await loadStoredConfig(runtime.configFile);
|
|
207
|
+
const stored = await dependencies.loadStoredConfig(runtime.configFile);
|
|
168
208
|
delete stored[key];
|
|
169
|
-
await saveStoredConfig(runtime.configFile, stored);
|
|
170
|
-
printJson(stored);
|
|
209
|
+
await dependencies.saveStoredConfig(runtime.configFile, stored);
|
|
210
|
+
dependencies.printJson(stored);
|
|
171
211
|
});
|
|
172
212
|
}
|
|
173
|
-
function buildTaskCommands(root) {
|
|
213
|
+
function buildTaskCommands(root, dependencies) {
|
|
174
214
|
const task = root.command("task").description("Task endpoints");
|
|
175
215
|
task
|
|
176
216
|
.command("get <projectId> <taskId>")
|
|
177
217
|
.description("Get a task by project id and task id")
|
|
178
218
|
.action(async (...args) => {
|
|
179
219
|
const [projectId, taskId] = args;
|
|
180
|
-
await runClientCommand(args, (client) => client.getTask(projectId, taskId));
|
|
220
|
+
await runClientCommand(args, dependencies, (client) => client.getTask(projectId, taskId));
|
|
181
221
|
});
|
|
182
222
|
withJsonBody(task
|
|
183
223
|
.command("create")
|
|
@@ -207,11 +247,11 @@ function buildTaskCommands(root) {
|
|
|
207
247
|
priority: options.priority,
|
|
208
248
|
sortOrder: options.sortOrder,
|
|
209
249
|
isAllDay: options.allDay,
|
|
210
|
-
}));
|
|
250
|
+
}, dependencies));
|
|
211
251
|
if (!payload.projectId || !payload.title) {
|
|
212
252
|
throw new Error("Task creation requires both projectId and title.");
|
|
213
253
|
}
|
|
214
|
-
await runClientCommand(args, (client) => client.createTask(payload));
|
|
254
|
+
await runClientCommand(args, dependencies, (client) => client.createTask(payload));
|
|
215
255
|
});
|
|
216
256
|
withJsonBody(task
|
|
217
257
|
.command("update <taskId>")
|
|
@@ -243,26 +283,26 @@ function buildTaskCommands(root) {
|
|
|
243
283
|
priority: options.priority,
|
|
244
284
|
sortOrder: options.sortOrder,
|
|
245
285
|
isAllDay: options.allDay,
|
|
246
|
-
}));
|
|
286
|
+
}, dependencies));
|
|
247
287
|
payload.id = taskId;
|
|
248
288
|
if (!payload.projectId) {
|
|
249
289
|
throw new Error("Task update requires projectId.");
|
|
250
290
|
}
|
|
251
|
-
await runClientCommand(args, (client) => client.updateTask(taskId, payload));
|
|
291
|
+
await runClientCommand(args, dependencies, (client) => client.updateTask(taskId, payload));
|
|
252
292
|
});
|
|
253
293
|
task
|
|
254
294
|
.command("complete <projectId> <taskId>")
|
|
255
295
|
.description("Complete a task")
|
|
256
296
|
.action(async (...args) => {
|
|
257
297
|
const [projectId, taskId] = args;
|
|
258
|
-
await runClientCommand(args, (client) => client.completeTask(projectId, taskId));
|
|
298
|
+
await runClientCommand(args, dependencies, (client) => client.completeTask(projectId, taskId));
|
|
259
299
|
});
|
|
260
300
|
task
|
|
261
301
|
.command("delete <projectId> <taskId>")
|
|
262
302
|
.description("Delete a task")
|
|
263
303
|
.action(async (...args) => {
|
|
264
304
|
const [projectId, taskId] = args;
|
|
265
|
-
await runClientCommand(args, (client) => client.deleteTask(projectId, taskId));
|
|
305
|
+
await runClientCommand(args, dependencies, (client) => client.deleteTask(projectId, taskId));
|
|
266
306
|
});
|
|
267
307
|
withJsonBody(task
|
|
268
308
|
.command("move")
|
|
@@ -272,7 +312,7 @@ function buildTaskCommands(root) {
|
|
|
272
312
|
.option("--task-id <id>", "Task id")).action(async (...args) => {
|
|
273
313
|
const command = args.at(-1);
|
|
274
314
|
const options = command.optsWithGlobals();
|
|
275
|
-
let payload = (await loadJsonValue(options.json, options.jsonFile));
|
|
315
|
+
let payload = (await dependencies.loadJsonValue(options.json, options.jsonFile));
|
|
276
316
|
if (!payload) {
|
|
277
317
|
if (!options.fromProjectId || !options.toProjectId || !options.taskId) {
|
|
278
318
|
throw new Error("Provide --json/--json-file or all of --from-project-id, --to-project-id, and --task-id.");
|
|
@@ -288,7 +328,7 @@ function buildTaskCommands(root) {
|
|
|
288
328
|
if (!Array.isArray(payload)) {
|
|
289
329
|
throw new Error("Move payload must be a JSON array.");
|
|
290
330
|
}
|
|
291
|
-
await runClientCommand(args, (client) => client.moveTasks(payload));
|
|
331
|
+
await runClientCommand(args, dependencies, (client) => client.moveTasks(payload));
|
|
292
332
|
});
|
|
293
333
|
withJsonBody(task
|
|
294
334
|
.command("completed")
|
|
@@ -302,8 +342,8 @@ function buildTaskCommands(root) {
|
|
|
302
342
|
projectIds: options.projectId && options.projectId.length > 0 ? options.projectId : undefined,
|
|
303
343
|
startDate: options.startDate,
|
|
304
344
|
endDate: options.endDate,
|
|
305
|
-
}));
|
|
306
|
-
await runClientCommand(args, (client) => client.listCompletedTasks(payload));
|
|
345
|
+
}, dependencies));
|
|
346
|
+
await runClientCommand(args, dependencies, (client) => client.listCompletedTasks(payload));
|
|
307
347
|
});
|
|
308
348
|
withJsonBody(task
|
|
309
349
|
.command("filter")
|
|
@@ -323,31 +363,31 @@ function buildTaskCommands(root) {
|
|
|
323
363
|
priority: options.priority && options.priority.length > 0 ? options.priority : undefined,
|
|
324
364
|
tag: options.tag && options.tag.length > 0 ? options.tag : undefined,
|
|
325
365
|
status: options.status && options.status.length > 0 ? options.status : undefined,
|
|
326
|
-
}));
|
|
327
|
-
await runClientCommand(args, (client) => client.filterTasks(payload));
|
|
366
|
+
}, dependencies));
|
|
367
|
+
await runClientCommand(args, dependencies, (client) => client.filterTasks(payload));
|
|
328
368
|
});
|
|
329
369
|
}
|
|
330
|
-
function buildProjectCommands(root) {
|
|
370
|
+
function buildProjectCommands(root, dependencies) {
|
|
331
371
|
const project = root.command("project").description("Project endpoints");
|
|
332
372
|
project
|
|
333
373
|
.command("list")
|
|
334
374
|
.description("List user projects")
|
|
335
375
|
.action(async (...args) => {
|
|
336
|
-
await runClientCommand(args, (client) => client.listProjects());
|
|
376
|
+
await runClientCommand(args, dependencies, (client) => client.listProjects());
|
|
337
377
|
});
|
|
338
378
|
project
|
|
339
379
|
.command("get <projectId>")
|
|
340
380
|
.description("Get a project by id")
|
|
341
381
|
.action(async (...args) => {
|
|
342
382
|
const [projectId] = args;
|
|
343
|
-
await runClientCommand(args, (client) => client.getProject(projectId));
|
|
383
|
+
await runClientCommand(args, dependencies, (client) => client.getProject(projectId));
|
|
344
384
|
});
|
|
345
385
|
project
|
|
346
386
|
.command("data <projectId>")
|
|
347
387
|
.description("Get a project together with tasks and columns")
|
|
348
388
|
.action(async (...args) => {
|
|
349
389
|
const [projectId] = args;
|
|
350
|
-
await runClientCommand(args, (client) => client.getProjectData(projectId));
|
|
390
|
+
await runClientCommand(args, dependencies, (client) => client.getProjectData(projectId));
|
|
351
391
|
});
|
|
352
392
|
withJsonBody(project
|
|
353
393
|
.command("create")
|
|
@@ -365,11 +405,11 @@ function buildProjectCommands(root) {
|
|
|
365
405
|
sortOrder: options.sortOrder,
|
|
366
406
|
viewMode: options.viewMode,
|
|
367
407
|
kind: options.kind,
|
|
368
|
-
}));
|
|
408
|
+
}, dependencies));
|
|
369
409
|
if (!payload.name) {
|
|
370
410
|
throw new Error("Project creation requires name.");
|
|
371
411
|
}
|
|
372
|
-
await runClientCommand(args, (client) => client.createProject(payload));
|
|
412
|
+
await runClientCommand(args, dependencies, (client) => client.createProject(payload));
|
|
373
413
|
});
|
|
374
414
|
withJsonBody(project
|
|
375
415
|
.command("update <projectId>")
|
|
@@ -388,18 +428,18 @@ function buildProjectCommands(root) {
|
|
|
388
428
|
sortOrder: options.sortOrder,
|
|
389
429
|
viewMode: options.viewMode,
|
|
390
430
|
kind: options.kind,
|
|
391
|
-
}));
|
|
392
|
-
await runClientCommand(args, (client) => client.updateProject(projectId, payload));
|
|
431
|
+
}, dependencies));
|
|
432
|
+
await runClientCommand(args, dependencies, (client) => client.updateProject(projectId, payload));
|
|
393
433
|
});
|
|
394
434
|
project
|
|
395
435
|
.command("delete <projectId>")
|
|
396
436
|
.description("Delete a project")
|
|
397
437
|
.action(async (...args) => {
|
|
398
438
|
const [projectId] = args;
|
|
399
|
-
await runClientCommand(args, (client) => client.deleteProject(projectId));
|
|
439
|
+
await runClientCommand(args, dependencies, (client) => client.deleteProject(projectId));
|
|
400
440
|
});
|
|
401
441
|
}
|
|
402
|
-
function buildRequestCommand(root) {
|
|
442
|
+
function buildRequestCommand(root, dependencies) {
|
|
403
443
|
withJsonBody(root
|
|
404
444
|
.command("request <method> <path>")
|
|
405
445
|
.description("Send a raw request to the configured API base URL, or to a full URL")
|
|
@@ -407,11 +447,13 @@ function buildRequestCommand(root) {
|
|
|
407
447
|
const command = args.at(-1);
|
|
408
448
|
const [method, path] = args;
|
|
409
449
|
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
|
-
|
|
450
|
+
const config = await dependencies.resolveRuntimeConfig(runtimeOverrides(options, dependencies));
|
|
451
|
+
const client = dependencies.createClient(config);
|
|
452
|
+
const body = await dependencies.loadJsonValue(options.json, options.jsonFile);
|
|
453
|
+
const result = await client.requestRaw(method.toUpperCase(), path, body, {
|
|
454
|
+
auth: options.auth,
|
|
455
|
+
});
|
|
456
|
+
dependencies.printJson(result ?? { ok: true });
|
|
415
457
|
});
|
|
416
458
|
}
|
|
417
459
|
function withJsonBody(command) {
|
|
@@ -419,10 +461,10 @@ function withJsonBody(command) {
|
|
|
419
461
|
.option("--json <json>", "Inline JSON body")
|
|
420
462
|
.option("--json-file <path>", "Path to a JSON body file");
|
|
421
463
|
}
|
|
422
|
-
function runtimeOverrides(options) {
|
|
464
|
+
function runtimeOverrides(options, dependencies) {
|
|
423
465
|
return {
|
|
424
466
|
configFile: options.configFile,
|
|
425
|
-
service: options.service ? validateService(options.service) : undefined,
|
|
467
|
+
service: options.service ? dependencies.validateService(options.service) : undefined,
|
|
426
468
|
apiBaseUrl: options.apiBaseUrl,
|
|
427
469
|
authBaseUrl: options.authBaseUrl,
|
|
428
470
|
accessToken: options.accessToken,
|
|
@@ -432,28 +474,28 @@ function runtimeOverrides(options) {
|
|
|
432
474
|
scopes: options.scopes,
|
|
433
475
|
};
|
|
434
476
|
}
|
|
435
|
-
async function runClientCommand(args, runner) {
|
|
477
|
+
async function runClientCommand(args, dependencies, runner) {
|
|
436
478
|
const command = args.at(-1);
|
|
437
|
-
const config = await resolveRuntimeConfig(runtimeOverrides(command.optsWithGlobals()));
|
|
438
|
-
const client =
|
|
479
|
+
const config = await dependencies.resolveRuntimeConfig(runtimeOverrides(command.optsWithGlobals(), dependencies));
|
|
480
|
+
const client = dependencies.createClient(config);
|
|
439
481
|
const result = await runner(client, config);
|
|
440
|
-
printJson(result ?? { ok: true });
|
|
482
|
+
dependencies.printJson(result ?? { ok: true });
|
|
441
483
|
}
|
|
442
|
-
async function loadObjectPayload(options, flags) {
|
|
443
|
-
const loaded = await loadJsonValue(options.json, options.jsonFile);
|
|
484
|
+
async function loadObjectPayload(options, flags, dependencies) {
|
|
485
|
+
const loaded = await dependencies.loadJsonValue(options.json, options.jsonFile);
|
|
444
486
|
if (loaded === undefined) {
|
|
445
|
-
return mergeDefined({}, flags);
|
|
487
|
+
return dependencies.mergeDefined({}, flags);
|
|
446
488
|
}
|
|
447
489
|
if (!isPlainObject(loaded)) {
|
|
448
490
|
throw new Error("Expected a JSON object payload.");
|
|
449
491
|
}
|
|
450
|
-
return mergeDefined(loaded, flags);
|
|
492
|
+
return dependencies.mergeDefined(loaded, flags);
|
|
451
493
|
}
|
|
452
494
|
function isPlainObject(value) {
|
|
453
495
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
454
496
|
}
|
|
455
|
-
async function persistConfig(runtime,
|
|
456
|
-
const stored = await loadStoredConfig(runtime.configFile);
|
|
497
|
+
async function persistConfig(runtime, token, dependencies) {
|
|
498
|
+
const stored = await dependencies.loadStoredConfig(runtime.configFile);
|
|
457
499
|
const next = {
|
|
458
500
|
...stored,
|
|
459
501
|
service: runtime.service,
|
|
@@ -463,9 +505,9 @@ async function persistConfig(runtime, accessToken) {
|
|
|
463
505
|
scopes: runtime.scopes,
|
|
464
506
|
apiBaseUrl: runtime.apiBaseUrl,
|
|
465
507
|
authBaseUrl: runtime.authBaseUrl,
|
|
466
|
-
accessToken,
|
|
508
|
+
accessToken: token.access_token,
|
|
467
509
|
};
|
|
468
|
-
await saveStoredConfig(runtime.configFile, next);
|
|
510
|
+
await dependencies.saveStoredConfig(runtime.configFile, next);
|
|
469
511
|
}
|
|
470
512
|
function requireClientCredentials(config) {
|
|
471
513
|
if (!config.clientId || !config.clientSecret) {
|
|
@@ -483,8 +525,9 @@ function collectString(value, previous = []) {
|
|
|
483
525
|
function collectInteger(value, previous = []) {
|
|
484
526
|
return [...previous, parseInteger(value)];
|
|
485
527
|
}
|
|
486
|
-
function
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
528
|
+
function isDirectExecution() {
|
|
529
|
+
return Boolean(process.argv[1]) && import.meta.url === pathToFileURL(process.argv[1]).href;
|
|
530
|
+
}
|
|
531
|
+
if (isDirectExecution()) {
|
|
532
|
+
await main();
|
|
490
533
|
}
|
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.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.0.2",
|
|
4
|
+
"description": "CLI wrapper for the TickTick Open API",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
7
7
|
"dist",
|
|
@@ -9,15 +9,15 @@
|
|
|
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
|
"check": "npm run build",
|
|
17
18
|
"clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"",
|
|
18
19
|
"dev": "tsx src/cli.ts",
|
|
19
|
-
"prepack": "npm run
|
|
20
|
-
"test": "npm run build && node --import tsx --test src/test/*.test.ts"
|
|
20
|
+
"prepack": "npm run build"
|
|
21
21
|
},
|
|
22
22
|
"keywords": [
|
|
23
23
|
"ticktick",
|