clipy-kit 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +65 -0
  3. package/dist/auth.d.ts +53 -0
  4. package/dist/auth.js +314 -0
  5. package/dist/cli.d.ts +6 -0
  6. package/dist/cli.js +160 -0
  7. package/dist/color.d.ts +12 -0
  8. package/dist/color.js +19 -0
  9. package/dist/colors.d.ts +6 -0
  10. package/dist/colors.js +26 -0
  11. package/dist/host/doctor.d.ts +17 -0
  12. package/dist/host/doctor.js +54 -0
  13. package/dist/host/help.d.ts +20 -0
  14. package/dist/host/help.js +142 -0
  15. package/dist/host/setup.d.ts +16 -0
  16. package/dist/host/setup.js +93 -0
  17. package/dist/http.d.ts +14 -0
  18. package/dist/http.js +53 -0
  19. package/dist/ids.d.ts +2 -0
  20. package/dist/ids.js +21 -0
  21. package/dist/json-input.d.ts +4 -0
  22. package/dist/json-input.js +34 -0
  23. package/dist/lib/format.d.ts +14 -0
  24. package/dist/lib/format.js +73 -0
  25. package/dist/lib/index.d.ts +6 -0
  26. package/dist/lib/index.js +6 -0
  27. package/dist/lib/ops.d.ts +460 -0
  28. package/dist/lib/ops.js +475 -0
  29. package/dist/lib/table.d.ts +14 -0
  30. package/dist/lib/table.js +67 -0
  31. package/dist/lib/types.d.ts +26 -0
  32. package/dist/lib/types.js +1 -0
  33. package/dist/output.d.ts +37 -0
  34. package/dist/output.js +67 -0
  35. package/dist/plugins/catalog.d.ts +11 -0
  36. package/dist/plugins/catalog.js +31 -0
  37. package/dist/plugins/sheet/plugin.d.ts +2 -0
  38. package/dist/plugins/sheet/plugin.js +8 -0
  39. package/dist/plugins/sheet/register.d.ts +2 -0
  40. package/dist/plugins/sheet/register.js +529 -0
  41. package/dist/plugins/sheet/specs.d.ts +20 -0
  42. package/dist/plugins/sheet/specs.js +291 -0
  43. package/dist/plugins/types.d.ts +7 -0
  44. package/dist/plugins/types.js +1 -0
  45. package/dist/ranges.d.ts +20 -0
  46. package/dist/ranges.js +77 -0
  47. package/dist/run.d.ts +353 -0
  48. package/dist/run.js +36 -0
  49. package/dist/specs.d.ts +1 -0
  50. package/dist/specs.js +1 -0
  51. package/package.json +50 -0
  52. package/skills/clipy/SKILL.md +38 -0
  53. package/skills/clipy-sheet/SKILL.md +71 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Clipy contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,65 @@
1
+ # Clipy (`clp`)
2
+
3
+ Local CLI for humans and coding agents. One binary on PATH, **intent commands** (not raw Google/Jira JSON), CLI-only login. Agents should never rewrite API or OAuth code.
4
+
5
+ This is not a hosted 1,000-app catalog (see Composio). v1 ships **Google Sheets** only. Other plugins are listed as planned.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm i -g clipy-kit
11
+ clp doctor --json
12
+ clp setup # if `clp` is not on PATH (common with macOS npm prefixes)
13
+ ```
14
+
15
+ Or without a global install: `npx --package clipy-kit clp doctor --json`
16
+
17
+ From a clone:
18
+
19
+ ```bash
20
+ npm install
21
+ npm run build
22
+ node dist/cli.js setup
23
+ ```
24
+
25
+ Skills:
26
+
27
+ ```bash
28
+ npx skills add /path/to/this/repo --skill clipy -g
29
+ npx skills add /path/to/this/repo --skill clipy-sheet -g
30
+ ```
31
+
32
+ ## Auth (Sheets)
33
+
34
+ ```bash
35
+ clp sheet auth login --credentials ./client_secret.json
36
+ clp sheet auth login --service-account ./sa.json
37
+ clp sheet auth status --pretty
38
+ ```
39
+
40
+ Credentials: `~/.config/clipy/sheet/` (override `CLIPY_CONFIG_DIR` / `GSHEET_CONFIG_DIR`). Enable **Sheets API** and **Drive API** on the Google Cloud project.
41
+
42
+ ## Usage
43
+
44
+ ```bash
45
+ clp --help --json
46
+ clp plugin list --json
47
+ clp sheet read <id> --tab Sheet1 --limit 20
48
+ clp sheet upsert <id> --tab Sheet1 --key-col Name --rows '[{"Name":"Kopi"}]' --dry-run
49
+ clp sheet format header <id> --tab Sheet1
50
+ ```
51
+
52
+ Data commands print JSON on stdout (`{ ok, cmd, result }`). Host commands (`setup`, `doctor`, `--help`) use color on a TTY; pass `--json` for agents. `NO_COLOR=1` disables ANSI.
53
+
54
+ ## Programmatic
55
+
56
+ ```ts
57
+ import { createOps, GoogleClient, getAccessToken } from 'clipy-kit';
58
+
59
+ const ops = createOps(new GoogleClient(() => getAccessToken()));
60
+ await ops.readTable(id, { tab: 'Sheet1', limit: 20 });
61
+ ```
62
+
63
+ ## License
64
+
65
+ MIT
package/dist/auth.d.ts ADDED
@@ -0,0 +1,53 @@
1
+ export declare const SCOPES: string[];
2
+ export type AuthMethod = 'oauth' | 'service_account' | 'token' | 'adc';
3
+ export type AccountFile = {
4
+ method: AuthMethod;
5
+ email?: string;
6
+ scopes: string[];
7
+ createdAt: string;
8
+ oauth?: {
9
+ client_id: string;
10
+ client_secret: string;
11
+ refresh_token?: string;
12
+ access_token?: string;
13
+ expiry_date?: number;
14
+ };
15
+ serviceAccount?: Record<string, unknown>;
16
+ token?: string;
17
+ };
18
+ export type AuthStatus = {
19
+ loggedIn: boolean;
20
+ method?: AuthMethod;
21
+ email?: string;
22
+ scopes?: string[];
23
+ expiresAt?: string;
24
+ account: string;
25
+ configDir: string;
26
+ };
27
+ type ConfigFile = {
28
+ defaultAccount: string;
29
+ };
30
+ export declare function clipyHome(): string;
31
+ export declare function configDir(): string;
32
+ export declare function accountPath(name: string): string;
33
+ export declare function loadConfig(): ConfigFile;
34
+ export declare function resolveAccountName(flag?: string): string;
35
+ export declare function loadAccount(name: string): AccountFile | undefined;
36
+ export declare function saveAccount(name: string, account: AccountFile, makeDefault?: boolean): void;
37
+ export declare function deleteAccount(name: string): boolean;
38
+ export type LoginOpts = {
39
+ account: string;
40
+ credentials?: string;
41
+ serviceAccount?: string;
42
+ token?: string;
43
+ tokenFile?: string;
44
+ adc?: boolean;
45
+ };
46
+ export declare function login(opts: LoginOpts): Promise<AuthStatus>;
47
+ export declare function status(accountName?: string): Promise<AuthStatus>;
48
+ export declare function logout(accountName?: string): Promise<{
49
+ removed: boolean;
50
+ account: string;
51
+ }>;
52
+ export declare function getAccessToken(accountName?: string): Promise<string>;
53
+ export {};
package/dist/auth.js ADDED
@@ -0,0 +1,314 @@
1
+ import fs from 'node:fs';
2
+ import http from 'node:http';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import { spawn } from 'node:child_process';
6
+ import { GoogleAuth, JWT, OAuth2Client } from 'google-auth-library';
7
+ import { CliError, EXIT } from './output.js';
8
+ export const SCOPES = [
9
+ 'https://www.googleapis.com/auth/spreadsheets',
10
+ 'https://www.googleapis.com/auth/drive',
11
+ 'https://www.googleapis.com/auth/userinfo.email',
12
+ ];
13
+ export function clipyHome() {
14
+ return process.env.CLIPY_CONFIG_DIR || path.join(os.homedir(), '.config', 'clipy');
15
+ }
16
+ export function configDir() {
17
+ return process.env.GSHEET_CONFIG_DIR || process.env.CLIPY_SHEET_CONFIG_DIR || path.join(clipyHome(), 'sheet');
18
+ }
19
+ function accountsDir() {
20
+ return path.join(configDir(), 'accounts');
21
+ }
22
+ function configPath() {
23
+ return path.join(configDir(), 'config.json');
24
+ }
25
+ function oauthClientPath() {
26
+ return path.join(configDir(), 'oauth-client.json');
27
+ }
28
+ export function accountPath(name) {
29
+ return path.join(accountsDir(), `${name}.json`);
30
+ }
31
+ function ensureDir() {
32
+ fs.mkdirSync(accountsDir(), { recursive: true, mode: 0o700 });
33
+ }
34
+ function readJson(file) {
35
+ if (!fs.existsSync(file))
36
+ return undefined;
37
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
38
+ }
39
+ function writeSecret(file, data) {
40
+ ensureDir();
41
+ fs.writeFileSync(file, JSON.stringify(data, null, 2), { mode: 0o600 });
42
+ }
43
+ export function loadConfig() {
44
+ return readJson(configPath()) ?? { defaultAccount: 'default' };
45
+ }
46
+ function saveConfig(cfg) {
47
+ writeSecret(configPath(), cfg);
48
+ }
49
+ export function resolveAccountName(flag) {
50
+ return flag || process.env.GSHEET_ACCOUNT || loadConfig().defaultAccount || 'default';
51
+ }
52
+ export function loadAccount(name) {
53
+ return readJson(accountPath(name));
54
+ }
55
+ export function saveAccount(name, account, makeDefault = false) {
56
+ writeSecret(accountPath(name), account);
57
+ const cfg = loadConfig();
58
+ if (makeDefault || !cfg.defaultAccount) {
59
+ saveConfig({ defaultAccount: name });
60
+ }
61
+ }
62
+ export function deleteAccount(name) {
63
+ const file = accountPath(name);
64
+ if (!fs.existsSync(file))
65
+ return false;
66
+ fs.unlinkSync(file);
67
+ return true;
68
+ }
69
+ function extractOAuthClient(raw) {
70
+ const obj = raw;
71
+ const nested = (obj.installed || obj.web || obj);
72
+ const client_id = String(nested.client_id ?? '');
73
+ const client_secret = String(nested.client_secret ?? '');
74
+ if (!client_id || !client_secret) {
75
+ throw new CliError('VALIDATION_ERROR', 'OAuth credentials JSON must include client_id and client_secret');
76
+ }
77
+ return { client_id, client_secret };
78
+ }
79
+ function loadStoredOAuthClient() {
80
+ const fromEnv = process.env.GSHEET_CLIENT_ID && process.env.GSHEET_CLIENT_SECRET
81
+ ? { client_id: process.env.GSHEET_CLIENT_ID, client_secret: process.env.GSHEET_CLIENT_SECRET }
82
+ : undefined;
83
+ return fromEnv ?? readJson(oauthClientPath());
84
+ }
85
+ function saveOAuthClient(client) {
86
+ writeSecret(oauthClientPath(), client);
87
+ }
88
+ function openBrowser(url) {
89
+ const platform = process.platform;
90
+ if (platform === 'darwin')
91
+ spawn('open', [url], { stdio: 'ignore', detached: true }).unref();
92
+ else if (platform === 'win32')
93
+ spawn('cmd', ['/c', 'start', '', url], { stdio: 'ignore', detached: true }).unref();
94
+ else
95
+ spawn('xdg-open', [url], { stdio: 'ignore', detached: true }).unref();
96
+ }
97
+ async function fetchEmail(token) {
98
+ try {
99
+ const res = await fetch('https://www.googleapis.com/oauth2/v2/userinfo', {
100
+ headers: { Authorization: `Bearer ${token}` },
101
+ });
102
+ if (!res.ok)
103
+ return undefined;
104
+ const body = (await res.json());
105
+ return body.email;
106
+ }
107
+ catch {
108
+ return undefined;
109
+ }
110
+ }
111
+ function countLoginMethods(opts) {
112
+ return [opts.credentials, opts.serviceAccount, opts.token || opts.tokenFile, opts.adc].filter(Boolean).length;
113
+ }
114
+ export async function login(opts) {
115
+ if (countLoginMethods(opts) > 1) {
116
+ throw new CliError('VALIDATION_ERROR', 'Use only one of --credentials, --service-account, --token/--token-file, or --adc');
117
+ }
118
+ const name = opts.account;
119
+ let account;
120
+ if (opts.adc) {
121
+ const auth = new GoogleAuth({ scopes: SCOPES });
122
+ const client = await auth.getClient();
123
+ const tokenRes = await client.getAccessToken();
124
+ const token = typeof tokenRes === 'string' ? tokenRes : tokenRes?.token;
125
+ if (!token) {
126
+ throw new CliError('AUTH_ERROR', 'ADC did not return an access token. Run: gcloud auth application-default login', undefined, EXIT.AUTH);
127
+ }
128
+ const email = (await auth.getCredentials().catch(() => undefined))?.client_email ?? (await fetchEmail(token));
129
+ account = { method: 'adc', email, scopes: SCOPES, createdAt: new Date().toISOString() };
130
+ }
131
+ else if (opts.serviceAccount) {
132
+ const raw = JSON.parse(fs.readFileSync(opts.serviceAccount, 'utf8'));
133
+ if (raw.type !== 'service_account' || !raw.client_email || !raw.private_key) {
134
+ throw new CliError('VALIDATION_ERROR', 'Service account JSON is missing type/client_email/private_key');
135
+ }
136
+ account = {
137
+ method: 'service_account',
138
+ email: String(raw.client_email),
139
+ scopes: SCOPES,
140
+ createdAt: new Date().toISOString(),
141
+ serviceAccount: raw,
142
+ };
143
+ }
144
+ else if (opts.token || opts.tokenFile) {
145
+ const token = opts.token || fs.readFileSync(opts.tokenFile, 'utf8').trim();
146
+ if (!token)
147
+ throw new CliError('VALIDATION_ERROR', 'Token is empty');
148
+ const email = await fetchEmail(token);
149
+ account = { method: 'token', email, scopes: SCOPES, createdAt: new Date().toISOString(), token };
150
+ }
151
+ else {
152
+ let clientJson;
153
+ if (opts.credentials) {
154
+ clientJson = extractOAuthClient(JSON.parse(fs.readFileSync(opts.credentials, 'utf8')));
155
+ saveOAuthClient(clientJson);
156
+ }
157
+ else {
158
+ clientJson = loadStoredOAuthClient();
159
+ }
160
+ if (!clientJson) {
161
+ throw new CliError('AUTH_REQUIRED', 'No OAuth client stored. Run: clp sheet auth login --credentials ./client_secret.json', undefined, EXIT.AUTH);
162
+ }
163
+ account = await oauthLogin(clientJson);
164
+ }
165
+ saveAccount(name, account, true);
166
+ return status(name);
167
+ }
168
+ async function oauthLogin(clientJson) {
169
+ const server = http.createServer();
170
+ const port = await new Promise((resolve, reject) => {
171
+ server.listen(0, '127.0.0.1', () => {
172
+ const addr = server.address();
173
+ resolve(typeof addr === 'object' && addr ? addr.port : 0);
174
+ });
175
+ server.on('error', reject);
176
+ });
177
+ const redirectUri = `http://127.0.0.1:${port}/callback`;
178
+ const oauth = new OAuth2Client(clientJson.client_id, clientJson.client_secret, redirectUri);
179
+ const url = oauth.generateAuthUrl({
180
+ access_type: 'offline',
181
+ prompt: 'consent',
182
+ scope: SCOPES,
183
+ });
184
+ const code = await new Promise((resolve, reject) => {
185
+ server.on('request', (req, res) => {
186
+ const parsed = new URL(req.url ?? '/', `http://127.0.0.1:${port}`);
187
+ if (parsed.pathname !== '/callback' && parsed.pathname !== '/') {
188
+ res.statusCode = 404;
189
+ res.end();
190
+ return;
191
+ }
192
+ const err = parsed.searchParams.get('error');
193
+ const value = parsed.searchParams.get('code');
194
+ res.setHeader('Content-Type', 'text/html; charset=utf-8');
195
+ if (err || !value) {
196
+ res.end('<p>clp sheet login failed. You can close this tab.</p>');
197
+ reject(new CliError('AUTH_ERROR', `OAuth error: ${err || 'missing code'}`, undefined, EXIT.AUTH));
198
+ return;
199
+ }
200
+ res.end('<p>clp sheet login OK. You can close this tab and return to the terminal.</p>');
201
+ resolve(value);
202
+ });
203
+ openBrowser(url);
204
+ });
205
+ await new Promise((resolve) => server.close(() => resolve()));
206
+ const { tokens } = await oauth.getToken(code);
207
+ oauth.setCredentials(tokens);
208
+ const access = tokens.access_token ?? '';
209
+ const email = access ? await fetchEmail(access) : undefined;
210
+ return {
211
+ method: 'oauth',
212
+ email,
213
+ scopes: SCOPES,
214
+ createdAt: new Date().toISOString(),
215
+ oauth: {
216
+ client_id: clientJson.client_id,
217
+ client_secret: clientJson.client_secret,
218
+ refresh_token: tokens.refresh_token ?? undefined,
219
+ access_token: tokens.access_token ?? undefined,
220
+ expiry_date: tokens.expiry_date ?? undefined,
221
+ },
222
+ };
223
+ }
224
+ export async function status(accountName) {
225
+ const name = resolveAccountName(accountName);
226
+ const dir = configDir();
227
+ if (process.env.GSHEET_TOKEN) {
228
+ const email = await fetchEmail(process.env.GSHEET_TOKEN);
229
+ return {
230
+ loggedIn: true,
231
+ method: 'token',
232
+ email,
233
+ scopes: SCOPES,
234
+ account: name,
235
+ configDir: dir,
236
+ };
237
+ }
238
+ const account = loadAccount(name);
239
+ if (!account) {
240
+ return { loggedIn: false, account: name, configDir: dir };
241
+ }
242
+ let expiresAt;
243
+ if (account.oauth?.expiry_date)
244
+ expiresAt = new Date(account.oauth.expiry_date).toISOString();
245
+ return {
246
+ loggedIn: true,
247
+ method: account.method,
248
+ email: account.email,
249
+ scopes: account.scopes,
250
+ expiresAt,
251
+ account: name,
252
+ configDir: dir,
253
+ };
254
+ }
255
+ export async function logout(accountName) {
256
+ const name = resolveAccountName(accountName);
257
+ const removed = deleteAccount(name);
258
+ return { removed, account: name };
259
+ }
260
+ export async function getAccessToken(accountName) {
261
+ if (process.env.GSHEET_TOKEN)
262
+ return process.env.GSHEET_TOKEN;
263
+ const name = resolveAccountName(accountName);
264
+ const account = loadAccount(name);
265
+ if (!account) {
266
+ throw new CliError('AUTH_REQUIRED', 'Run: clp sheet auth login', { account: name }, EXIT.AUTH);
267
+ }
268
+ if (account.method === 'token') {
269
+ if (!account.token)
270
+ throw new CliError('AUTH_REQUIRED', 'Run: clp sheet auth login --token <token>', undefined, EXIT.AUTH);
271
+ return account.token;
272
+ }
273
+ if (account.method === 'service_account') {
274
+ const sa = account.serviceAccount;
275
+ const jwt = new JWT({
276
+ email: sa.client_email,
277
+ key: sa.private_key,
278
+ scopes: SCOPES,
279
+ });
280
+ const token = await jwt.getAccessToken();
281
+ const value = typeof token === 'string' ? token : token?.token;
282
+ if (!value) {
283
+ throw new CliError('AUTH_ERROR', 'Service account did not return an access token', undefined, EXIT.AUTH);
284
+ }
285
+ return value;
286
+ }
287
+ if (account.method === 'adc') {
288
+ const auth = new GoogleAuth({ scopes: SCOPES });
289
+ const token = await auth.getAccessToken();
290
+ if (!token) {
291
+ throw new CliError('AUTH_ERROR', 'ADC did not return an access token. Run: clp sheet auth login --adc', undefined, EXIT.AUTH);
292
+ }
293
+ return token;
294
+ }
295
+ if (account.method === 'oauth' && account.oauth) {
296
+ const oauth = new OAuth2Client(account.oauth.client_id, account.oauth.client_secret);
297
+ oauth.setCredentials({
298
+ refresh_token: account.oauth.refresh_token,
299
+ access_token: account.oauth.access_token,
300
+ expiry_date: account.oauth.expiry_date,
301
+ });
302
+ const token = await oauth.getAccessToken();
303
+ const value = typeof token === 'string' ? token : token?.token;
304
+ if (!value) {
305
+ throw new CliError('AUTH_REQUIRED', 'OAuth token expired. Run: clp sheet auth login', undefined, EXIT.AUTH);
306
+ }
307
+ const creds = oauth.credentials;
308
+ account.oauth.access_token = creds.access_token ?? account.oauth.access_token;
309
+ account.oauth.expiry_date = creds.expiry_date ?? account.oauth.expiry_date;
310
+ saveAccount(name, account);
311
+ return value;
312
+ }
313
+ throw new CliError('AUTH_REQUIRED', 'Run: clp sheet auth login', { account: name }, EXIT.AUTH);
314
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ export declare function jsonHelpRequested(argv: string[]): boolean;
4
+ export declare function commandPath(argv: string[]): string[];
5
+ export declare function buildProgram(): Command;
6
+ export declare function main(argv?: string[]): Promise<void>;
package/dist/cli.js ADDED
@@ -0,0 +1,160 @@
1
+ #!/usr/bin/env node
2
+ import { pathToFileURL } from 'node:url';
3
+ import { Command, CommanderError } from 'commander';
4
+ import { CliError, envelopeErr, envelopeOk, EXIT, exitCodeFor, stringifyEnvelope } from './output.js';
5
+ import { allPlugins, findPlugin } from './plugins/catalog.js';
6
+ import { sheetPlugin } from './plugins/sheet/plugin.js';
7
+ import { doctor, doctorErrorCode } from './host/doctor.js';
8
+ import { hostHelpJson, printDoctorHuman, printHumanHelp, printPluginListHuman, printSetupHuman, sheetHelpJson } from './host/help.js';
9
+ import { setup } from './host/setup.js';
10
+ import { c } from './color.js';
11
+ import { run, takeGlobals } from './run.js';
12
+ export function jsonHelpRequested(argv) {
13
+ return argv.includes('--json') && (argv.includes('--help') || argv.includes('-h'));
14
+ }
15
+ export function commandPath(argv) {
16
+ return argv.filter((a) => a !== '--' && !a.startsWith('-'));
17
+ }
18
+ function isHumanHostCommand(path) {
19
+ if (path[0] === 'setup' || path[0] === 'doctor')
20
+ return true;
21
+ if (path[0] === 'plugin' && (path[1] === 'list' || !path[1]))
22
+ return true;
23
+ return false;
24
+ }
25
+ function useJson(argv, path) {
26
+ if (argv.includes('--json'))
27
+ return true;
28
+ if (jsonHelpRequested(argv))
29
+ return true;
30
+ if (isHumanHostCommand(path) && process.stdout.isTTY)
31
+ return false;
32
+ if ((argv.includes('--help') || argv.includes('-h')) && process.stdout.isTTY)
33
+ return false;
34
+ return true;
35
+ }
36
+ export function buildProgram() {
37
+ const program = new Command();
38
+ program
39
+ .name('clp')
40
+ .description('Clipy. Plugins for Sheets and more. Never write API or OAuth code.')
41
+ .option('--pretty', 'Pretty-print JSON')
42
+ .option('--json', 'JSON envelope (always used for data commands)')
43
+ .option('--account <name>', 'Credential profile')
44
+ .option('--spreadsheet <id>', 'Default spreadsheet ID or URL')
45
+ .option('--dry-run', 'Preview writes without applying')
46
+ .helpOption('-h, --help', 'Show help. Combine with --json for machine schema.')
47
+ .showHelpAfterError(false)
48
+ .exitOverride();
49
+ program
50
+ .command('setup')
51
+ .description('Install clp on PATH')
52
+ .option('--undo', 'Remove shim and PATH block')
53
+ .action(async (opts, cmd) => {
54
+ const g = takeGlobals(cmd);
55
+ const argv = process.argv.slice(2);
56
+ const result = setup({ undo: Boolean(opts.undo) });
57
+ if (useJson(argv, ['setup']) || g.json) {
58
+ await run('setup', g.pretty, async () => ({ result }));
59
+ return;
60
+ }
61
+ printSetupHuman(result);
62
+ });
63
+ program
64
+ .command('doctor')
65
+ .description('Check PATH, Node, config, plugins')
66
+ .action(async (_opts, cmd) => {
67
+ const g = takeGlobals(cmd);
68
+ const argv = process.argv.slice(2);
69
+ const result = doctor();
70
+ const code = doctorErrorCode(result);
71
+ if (useJson(argv, ['doctor']) || g.json) {
72
+ if (!result.ok && code === 'PATH_REQUIRED') {
73
+ process.stdout.write(stringifyEnvelope(envelopeErr('doctor', new CliError('PATH_REQUIRED', 'Run: clp setup', result, EXIT.VALIDATION)), g.pretty) + '\n');
74
+ process.exitCode = EXIT.VALIDATION;
75
+ return;
76
+ }
77
+ await run('doctor', g.pretty, async () => ({ result }));
78
+ if (!result.ok)
79
+ process.exitCode = EXIT.VALIDATION;
80
+ return;
81
+ }
82
+ printDoctorHuman(result);
83
+ if (!result.ok)
84
+ process.exitCode = EXIT.VALIDATION;
85
+ });
86
+ const plugin = program.command('plugin').description('List or add plugins');
87
+ plugin.command('list').description('Built-in and planned plugins').action(async (_opts, cmd) => {
88
+ const g = takeGlobals(cmd);
89
+ const argv = process.argv.slice(2);
90
+ const result = { plugins: allPlugins() };
91
+ if (useJson(argv, ['plugin', 'list']) || g.json) {
92
+ await run('plugin list', g.pretty, async () => ({ result }));
93
+ return;
94
+ }
95
+ printPluginListHuman();
96
+ });
97
+ plugin
98
+ .command('add')
99
+ .description('Enable a plugin')
100
+ .argument('<name>', 'Plugin name')
101
+ .action(async (name, _opts, cmd) => {
102
+ const g = takeGlobals(cmd);
103
+ await run('plugin add', g.pretty, async () => {
104
+ const info = findPlugin(name);
105
+ if (info?.status === 'built-in') {
106
+ return { result: { name, status: 'built-in', message: `${name} is already built into clipy` } };
107
+ }
108
+ throw new CliError('PLUGIN_UNAVAILABLE', info?.status === 'planned'
109
+ ? `${name} is planned, not shipped. See: clp plugin list`
110
+ : `Unknown plugin: ${name}`, { name, status: info?.status });
111
+ });
112
+ });
113
+ const sheet = program.command(sheetPlugin.name).description(sheetPlugin.description);
114
+ sheetPlugin.register(sheet);
115
+ return program;
116
+ }
117
+ export async function main(argv = process.argv.slice(2)) {
118
+ const path = commandPath(argv);
119
+ const pretty = argv.includes('--pretty');
120
+ if (jsonHelpRequested(argv)) {
121
+ const body = path[0] === 'sheet' ? sheetHelpJson(path.slice(1)) : hostHelpJson(path);
122
+ process.stdout.write(stringifyEnvelope(envelopeOk('help', body), pretty) + '\n');
123
+ return;
124
+ }
125
+ if (argv.includes('--help') || argv.includes('-h')) {
126
+ if (!useJson(argv, path)) {
127
+ printHumanHelp(path);
128
+ return;
129
+ }
130
+ const body = path[0] === 'sheet' ? sheetHelpJson(path.slice(1)) : hostHelpJson(path);
131
+ process.stdout.write(stringifyEnvelope(envelopeOk('help', body), pretty) + '\n');
132
+ return;
133
+ }
134
+ const program = buildProgram();
135
+ try {
136
+ await program.parseAsync(['node', 'clp', ...argv]);
137
+ }
138
+ catch (err) {
139
+ if (err instanceof CommanderError && (err.code === 'commander.helpDisplayed' || err.code === 'commander.version')) {
140
+ if (!useJson(argv, path) && process.stdout.isTTY) {
141
+ printHumanHelp(path);
142
+ return;
143
+ }
144
+ return;
145
+ }
146
+ const cmd = path.join(' ') || 'clp';
147
+ const wrapped = err instanceof CliError ? err : new CliError('VALIDATION_ERROR', err instanceof Error ? err.message : String(err));
148
+ if (!useJson(argv, path) && wrapped.code !== 'AUTH_REQUIRED' && isHumanHostCommand(path)) {
149
+ console.error(c.err(wrapped.message));
150
+ process.exitCode = exitCodeFor(wrapped);
151
+ return;
152
+ }
153
+ process.stdout.write(stringifyEnvelope(envelopeErr(cmd, wrapped), pretty) + '\n');
154
+ process.exitCode = err instanceof CommanderError ? EXIT.VALIDATION : exitCodeFor(wrapped);
155
+ }
156
+ }
157
+ const isDirectRun = process.argv[1] !== undefined && import.meta.url === pathToFileURL(process.argv[1]).href;
158
+ if (isDirectRun) {
159
+ void main();
160
+ }
@@ -0,0 +1,12 @@
1
+ export declare function colorEnabled(stream?: {
2
+ isTTY?: boolean;
3
+ }): boolean;
4
+ export declare const c: {
5
+ plugin: (s: string) => string;
6
+ ok: (s: string) => string;
7
+ warn: (s: string) => string;
8
+ err: (s: string) => string;
9
+ dim: (s: string) => string;
10
+ bold: (s: string) => string;
11
+ title: (s: string) => string;
12
+ };
package/dist/color.js ADDED
@@ -0,0 +1,19 @@
1
+ import pc from 'picocolors';
2
+ export function colorEnabled(stream = process.stdout) {
3
+ if (process.env.NO_COLOR !== undefined && process.env.NO_COLOR !== '')
4
+ return false;
5
+ if (process.env.FORCE_COLOR === '0')
6
+ return false;
7
+ if (process.env.FORCE_COLOR)
8
+ return true;
9
+ return Boolean(stream.isTTY);
10
+ }
11
+ export const c = {
12
+ plugin: (s) => (colorEnabled() ? pc.cyan(s) : s),
13
+ ok: (s) => (colorEnabled() ? pc.green(s) : s),
14
+ warn: (s) => (colorEnabled() ? pc.yellow(s) : s),
15
+ err: (s) => (colorEnabled() ? pc.red(s) : s),
16
+ dim: (s) => (colorEnabled() ? pc.dim(s) : s),
17
+ bold: (s) => (colorEnabled() ? pc.bold(s) : s),
18
+ title: (s) => (colorEnabled() ? pc.bold(pc.white(s)) : s),
19
+ };
@@ -0,0 +1,6 @@
1
+ export type Rgb = {
2
+ red: number;
3
+ green: number;
4
+ blue: number;
5
+ };
6
+ export declare function parseColor(input: string): Rgb;