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.
- package/LICENSE +21 -0
- package/README.md +65 -0
- package/dist/auth.d.ts +53 -0
- package/dist/auth.js +314 -0
- package/dist/cli.d.ts +6 -0
- package/dist/cli.js +160 -0
- package/dist/color.d.ts +12 -0
- package/dist/color.js +19 -0
- package/dist/colors.d.ts +6 -0
- package/dist/colors.js +26 -0
- package/dist/host/doctor.d.ts +17 -0
- package/dist/host/doctor.js +54 -0
- package/dist/host/help.d.ts +20 -0
- package/dist/host/help.js +142 -0
- package/dist/host/setup.d.ts +16 -0
- package/dist/host/setup.js +93 -0
- package/dist/http.d.ts +14 -0
- package/dist/http.js +53 -0
- package/dist/ids.d.ts +2 -0
- package/dist/ids.js +21 -0
- package/dist/json-input.d.ts +4 -0
- package/dist/json-input.js +34 -0
- package/dist/lib/format.d.ts +14 -0
- package/dist/lib/format.js +73 -0
- package/dist/lib/index.d.ts +6 -0
- package/dist/lib/index.js +6 -0
- package/dist/lib/ops.d.ts +460 -0
- package/dist/lib/ops.js +475 -0
- package/dist/lib/table.d.ts +14 -0
- package/dist/lib/table.js +67 -0
- package/dist/lib/types.d.ts +26 -0
- package/dist/lib/types.js +1 -0
- package/dist/output.d.ts +37 -0
- package/dist/output.js +67 -0
- package/dist/plugins/catalog.d.ts +11 -0
- package/dist/plugins/catalog.js +31 -0
- package/dist/plugins/sheet/plugin.d.ts +2 -0
- package/dist/plugins/sheet/plugin.js +8 -0
- package/dist/plugins/sheet/register.d.ts +2 -0
- package/dist/plugins/sheet/register.js +529 -0
- package/dist/plugins/sheet/specs.d.ts +20 -0
- package/dist/plugins/sheet/specs.js +291 -0
- package/dist/plugins/types.d.ts +7 -0
- package/dist/plugins/types.js +1 -0
- package/dist/ranges.d.ts +20 -0
- package/dist/ranges.js +77 -0
- package/dist/run.d.ts +353 -0
- package/dist/run.js +36 -0
- package/dist/specs.d.ts +1 -0
- package/dist/specs.js +1 -0
- package/package.json +50 -0
- package/skills/clipy/SKILL.md +38 -0
- package/skills/clipy-sheet/SKILL.md +71 -0
package/dist/colors.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { CliError } from './output.js';
|
|
2
|
+
const NAMED = {
|
|
3
|
+
white: '#ffffff',
|
|
4
|
+
black: '#000000',
|
|
5
|
+
navy: '#1a365d',
|
|
6
|
+
red: '#c53030',
|
|
7
|
+
green: '#276749',
|
|
8
|
+
blue: '#2b6cb0',
|
|
9
|
+
yellow: '#d69e2e',
|
|
10
|
+
gray: '#718096',
|
|
11
|
+
grey: '#718096',
|
|
12
|
+
orange: '#dd6b20',
|
|
13
|
+
purple: '#6b46c1',
|
|
14
|
+
};
|
|
15
|
+
export function parseColor(input) {
|
|
16
|
+
const key = input.trim().toLowerCase();
|
|
17
|
+
const hex = (NAMED[key] ?? (key.startsWith('#') ? key : `#${key}`)).replace('#', '');
|
|
18
|
+
if (!/^[0-9a-fA-F]{6}$/.test(hex)) {
|
|
19
|
+
throw new CliError('VALIDATION_ERROR', `Invalid color: ${input}. Use #rrggbb or a name like white, navy.`);
|
|
20
|
+
}
|
|
21
|
+
return {
|
|
22
|
+
red: parseInt(hex.slice(0, 2), 16) / 255,
|
|
23
|
+
green: parseInt(hex.slice(2, 4), 16) / 255,
|
|
24
|
+
blue: parseInt(hex.slice(4, 6), 16) / 255,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export declare function clipyHome(): string;
|
|
2
|
+
export declare function doctor(): {
|
|
3
|
+
ok: boolean;
|
|
4
|
+
onPath: boolean;
|
|
5
|
+
which?: string;
|
|
6
|
+
binDir: string;
|
|
7
|
+
node: string;
|
|
8
|
+
nodeOk: boolean;
|
|
9
|
+
configDir: string;
|
|
10
|
+
configWritable: boolean;
|
|
11
|
+
plugins: {
|
|
12
|
+
builtIn: string[];
|
|
13
|
+
planned: number;
|
|
14
|
+
};
|
|
15
|
+
hint?: string;
|
|
16
|
+
};
|
|
17
|
+
export declare function doctorErrorCode(result: ReturnType<typeof doctor>): string | undefined;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { spawnSync } from 'node:child_process';
|
|
5
|
+
import { allPlugins, BUILTIN } from '../plugins/catalog.js';
|
|
6
|
+
import { binDir, pathHasDir } from './setup.js';
|
|
7
|
+
export function clipyHome() {
|
|
8
|
+
return process.env.CLIPY_CONFIG_DIR || path.join(os.homedir(), '.config', 'clipy');
|
|
9
|
+
}
|
|
10
|
+
function whichClp() {
|
|
11
|
+
const result = spawnSync('which', ['clp'], { encoding: 'utf8' });
|
|
12
|
+
const loc = result.stdout?.trim();
|
|
13
|
+
return loc || undefined;
|
|
14
|
+
}
|
|
15
|
+
export function doctor() {
|
|
16
|
+
const node = process.version;
|
|
17
|
+
const major = Number(node.slice(1).split('.')[0]);
|
|
18
|
+
const nodeOk = major >= 20;
|
|
19
|
+
const dir = binDir();
|
|
20
|
+
const which = whichClp();
|
|
21
|
+
const onPath = pathHasDir(dir) || Boolean(which && path.resolve(path.dirname(which)) === path.resolve(dir));
|
|
22
|
+
const home = clipyHome();
|
|
23
|
+
let configWritable = false;
|
|
24
|
+
try {
|
|
25
|
+
fs.mkdirSync(home, { recursive: true });
|
|
26
|
+
fs.accessSync(home, fs.constants.W_OK);
|
|
27
|
+
configWritable = true;
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
configWritable = false;
|
|
31
|
+
}
|
|
32
|
+
const ok = onPath && nodeOk && configWritable;
|
|
33
|
+
return {
|
|
34
|
+
ok,
|
|
35
|
+
onPath,
|
|
36
|
+
which,
|
|
37
|
+
binDir: dir,
|
|
38
|
+
node,
|
|
39
|
+
nodeOk,
|
|
40
|
+
configDir: home,
|
|
41
|
+
configWritable,
|
|
42
|
+
plugins: { builtIn: BUILTIN.map((p) => p.name), planned: allPlugins().filter((p) => p.status === 'planned').length },
|
|
43
|
+
hint: onPath ? undefined : 'Run: clp setup',
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
export function doctorErrorCode(result) {
|
|
47
|
+
if (!result.onPath)
|
|
48
|
+
return 'PATH_REQUIRED';
|
|
49
|
+
if (!result.nodeOk)
|
|
50
|
+
return 'NODE_VERSION';
|
|
51
|
+
if (!result.configWritable)
|
|
52
|
+
return 'CONFIG_ERROR';
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export declare function hostHelpJson(path?: string[]): unknown;
|
|
2
|
+
export declare function sheetHelpJson(path?: string[]): unknown;
|
|
3
|
+
export declare function printHumanHelp(path: string[]): void;
|
|
4
|
+
export declare function printPluginListHuman(): void;
|
|
5
|
+
export declare function printDoctorHuman(d: {
|
|
6
|
+
ok: boolean;
|
|
7
|
+
onPath: boolean;
|
|
8
|
+
which?: string;
|
|
9
|
+
binDir: string;
|
|
10
|
+
node: string;
|
|
11
|
+
nodeOk: boolean;
|
|
12
|
+
configDir: string;
|
|
13
|
+
configWritable: boolean;
|
|
14
|
+
hint?: string;
|
|
15
|
+
}): void;
|
|
16
|
+
export declare function printSetupHuman(result: {
|
|
17
|
+
bin: string;
|
|
18
|
+
hint?: string;
|
|
19
|
+
undo?: boolean;
|
|
20
|
+
}): void;
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { BUILTIN, PLANNED, allPlugins } from '../plugins/catalog.js';
|
|
2
|
+
import { COMMANDS, findSpec } from '../plugins/sheet/specs.js';
|
|
3
|
+
import { c } from '../color.js';
|
|
4
|
+
export function hostHelpJson(path = []) {
|
|
5
|
+
if (!path.length || (path.length === 1 && path[0] === 'plugin')) {
|
|
6
|
+
return {
|
|
7
|
+
name: 'clp',
|
|
8
|
+
description: 'Clipy. One PATH binary; plugins for Sheets and (later) Linear, GitHub, Jira. Never write API or OAuth code — run clp <plugin>.',
|
|
9
|
+
globalOptions: [
|
|
10
|
+
{ flags: '--pretty', description: 'Pretty-print JSON' },
|
|
11
|
+
{ flags: '--json', description: 'Machine JSON (default for data commands; use with setup/doctor/plugin list)' },
|
|
12
|
+
{ flags: '--account <name>', description: 'Credential profile (plugin-specific)' },
|
|
13
|
+
{ flags: '--dry-run', description: 'Preview writes' },
|
|
14
|
+
{ flags: '--help --json', description: 'Machine-readable schema' },
|
|
15
|
+
],
|
|
16
|
+
commands: [
|
|
17
|
+
{ name: 'setup', description: 'Install clp on PATH (~/.local/bin) and optionally update shell rc' },
|
|
18
|
+
{ name: 'doctor', description: 'Check PATH, Node, config, plugins' },
|
|
19
|
+
{ name: 'plugin', description: 'list | add', subcommands: ['list', 'add'] },
|
|
20
|
+
{ name: 'sheet', description: BUILTIN[0]?.description },
|
|
21
|
+
],
|
|
22
|
+
plugins: allPlugins(),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
if (path[0] === 'setup') {
|
|
26
|
+
return { name: 'setup', description: 'Install the clp shim on PATH', options: [{ flags: '--undo', description: 'Remove shim and PATH block' }] };
|
|
27
|
+
}
|
|
28
|
+
if (path[0] === 'doctor') {
|
|
29
|
+
return { name: 'doctor', description: 'Health check. JSON with --json. Colored text on TTY.' };
|
|
30
|
+
}
|
|
31
|
+
if (path[0] === 'plugin') {
|
|
32
|
+
return {
|
|
33
|
+
name: 'plugin',
|
|
34
|
+
subcommands: [
|
|
35
|
+
{ name: 'list', description: 'Built-in and planned plugins' },
|
|
36
|
+
{ name: 'add', description: 'Enable a plugin (v1: only sheet is built-in)', args: [{ name: 'name', required: true }] },
|
|
37
|
+
],
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
return { error: { code: 'UNKNOWN_COMMAND', message: `No host command: ${path.join(' ')}`, commands: ['setup', 'doctor', 'plugin', 'sheet'] } };
|
|
41
|
+
}
|
|
42
|
+
export function sheetHelpJson(path = []) {
|
|
43
|
+
if (!path.length) {
|
|
44
|
+
return {
|
|
45
|
+
name: 'sheet',
|
|
46
|
+
description: 'Google Sheets plugin. Never write googleapis or OAuth — run clp sheet …',
|
|
47
|
+
commands: COMMANDS.map((spec) => ({
|
|
48
|
+
name: spec.name,
|
|
49
|
+
description: spec.description,
|
|
50
|
+
subcommands: spec.subcommands?.map((s) => s.name),
|
|
51
|
+
})),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
const spec = findSpec(path);
|
|
55
|
+
if (!spec) {
|
|
56
|
+
return { error: { code: 'UNKNOWN_COMMAND', message: `No command: sheet ${path.join(' ')}`, commands: COMMANDS.map((c) => c.name) } };
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
name: spec.name,
|
|
60
|
+
description: spec.description,
|
|
61
|
+
args: spec.args ?? [],
|
|
62
|
+
options: spec.options ?? [],
|
|
63
|
+
subcommands: spec.subcommands?.map((s) => ({
|
|
64
|
+
name: s.name,
|
|
65
|
+
description: s.description,
|
|
66
|
+
args: s.args ?? [],
|
|
67
|
+
options: s.options ?? [],
|
|
68
|
+
})),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
export function printHumanHelp(path) {
|
|
72
|
+
if (path[0] === 'sheet') {
|
|
73
|
+
const specPath = path.slice(1);
|
|
74
|
+
if (specPath.length) {
|
|
75
|
+
const spec = findSpec(specPath);
|
|
76
|
+
if (!spec) {
|
|
77
|
+
console.log(c.err(`Unknown: clp sheet ${specPath.join(' ')}`));
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
console.log(c.title(`clp sheet ${specPath.join(' ')}`));
|
|
81
|
+
console.log(c.dim(spec.description));
|
|
82
|
+
if (spec.options?.length) {
|
|
83
|
+
console.log(`\n${c.bold('Flags')}`);
|
|
84
|
+
for (const o of spec.options)
|
|
85
|
+
console.log(` ${c.plugin(o.flags)} ${o.description}`);
|
|
86
|
+
}
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
console.log(c.title('clp sheet') + c.dim(' Google Sheets'));
|
|
90
|
+
console.log(c.dim('JSON on stdout. Login: clp sheet auth login\n'));
|
|
91
|
+
for (const spec of COMMANDS) {
|
|
92
|
+
console.log(` ${c.plugin(spec.name.padEnd(16))} ${spec.description}`);
|
|
93
|
+
}
|
|
94
|
+
console.log(`\n${c.dim('Schema: clp sheet <cmd> --help --json')}`);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
console.log(c.title('clp') + ' ' + c.dim('Clipy'));
|
|
98
|
+
console.log(c.dim('One binary for humans and agents. Never rewrite API or OAuth code.\n'));
|
|
99
|
+
console.log(c.bold('Commands'));
|
|
100
|
+
console.log(` ${c.plugin('setup'.padEnd(16))} Install clp on PATH`);
|
|
101
|
+
console.log(` ${c.plugin('doctor'.padEnd(16))} Check PATH, Node, config`);
|
|
102
|
+
console.log(` ${c.plugin('plugin list'.padEnd(16))} Built-in and planned plugins`);
|
|
103
|
+
console.log(` ${c.plugin('sheet …'.padEnd(16))} Google Sheets\n`);
|
|
104
|
+
console.log(c.bold('Plugins'));
|
|
105
|
+
for (const p of BUILTIN) {
|
|
106
|
+
console.log(` ${c.ok('●')} ${c.plugin(p.name.padEnd(16))} ${p.description}`);
|
|
107
|
+
}
|
|
108
|
+
for (const p of PLANNED.slice(0, 6)) {
|
|
109
|
+
console.log(` ${c.dim('○')} ${c.dim(p.name.padEnd(16))} ${c.dim(p.description + ' (planned)')}`);
|
|
110
|
+
}
|
|
111
|
+
console.log(c.dim(` … ${Math.max(0, PLANNED.length - 6)} more planned. See: clp plugin list\n`));
|
|
112
|
+
console.log(c.dim('Machine schema: clp --help --json'));
|
|
113
|
+
}
|
|
114
|
+
export function printPluginListHuman() {
|
|
115
|
+
console.log(c.title('Plugins') + '\n');
|
|
116
|
+
console.log(c.bold('Built-in'));
|
|
117
|
+
for (const p of BUILTIN) {
|
|
118
|
+
console.log(` ${c.ok('●')} ${c.plugin(p.name)} ${p.description}`);
|
|
119
|
+
}
|
|
120
|
+
console.log(`\n${c.bold('Planned')} ${c.dim('(not installed — do not call)')}`);
|
|
121
|
+
for (const p of PLANNED) {
|
|
122
|
+
console.log(` ${c.dim('○')} ${c.dim(p.name.padEnd(16))} ${c.dim(`[${p.when}] ${p.description}`)}`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
export function printDoctorHuman(d) {
|
|
126
|
+
const row = (ok, label, value) => `${ok ? c.ok('✔') : c.err('✖')} ${label.padEnd(16)} ${ok ? value : c.warn(value)}`;
|
|
127
|
+
console.log(c.title('clp doctor') + '\n');
|
|
128
|
+
console.log(row(d.nodeOk, 'Node', d.node));
|
|
129
|
+
console.log(row(d.onPath, 'PATH', d.which || d.binDir));
|
|
130
|
+
console.log(row(d.configWritable, 'Config', d.configDir));
|
|
131
|
+
console.log(`\n${d.ok ? c.ok('All good.') : c.warn(d.hint || 'See failures above.')}`);
|
|
132
|
+
}
|
|
133
|
+
export function printSetupHuman(result) {
|
|
134
|
+
if (result.undo) {
|
|
135
|
+
console.log(c.warn('Removed clp shim.') + (result.hint ? `\n${c.dim(result.hint)}` : ''));
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
console.log(c.ok(`Installed ${result.bin}`));
|
|
139
|
+
if (result.hint)
|
|
140
|
+
console.log(c.dim(result.hint));
|
|
141
|
+
console.log(c.dim('Next: clp doctor'));
|
|
142
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export declare function binDir(): string;
|
|
2
|
+
export declare function shellRcPath(): string;
|
|
3
|
+
export declare function cliEntryPath(): string;
|
|
4
|
+
export declare function pathHasDir(dir: string, pathEnv?: string): boolean;
|
|
5
|
+
export type SetupResult = {
|
|
6
|
+
bin: string;
|
|
7
|
+
target: string;
|
|
8
|
+
shim: string;
|
|
9
|
+
pathConfigured: boolean;
|
|
10
|
+
rcUpdated?: string;
|
|
11
|
+
undo?: boolean;
|
|
12
|
+
hint?: string;
|
|
13
|
+
};
|
|
14
|
+
export declare function setup(opts?: {
|
|
15
|
+
undo?: boolean;
|
|
16
|
+
}): SetupResult;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { CliError, EXIT } from '../output.js';
|
|
6
|
+
const MARKER_START = '# clipy cli';
|
|
7
|
+
const MARKER_END = '# end clipy cli';
|
|
8
|
+
export function binDir() {
|
|
9
|
+
return process.env.CLIPY_BIN_DIR || path.join(os.homedir(), '.local', 'bin');
|
|
10
|
+
}
|
|
11
|
+
export function shellRcPath() {
|
|
12
|
+
if (process.env.CLIPY_SHELL_RC)
|
|
13
|
+
return process.env.CLIPY_SHELL_RC;
|
|
14
|
+
const shell = process.env.SHELL || '';
|
|
15
|
+
if (shell.includes('zsh'))
|
|
16
|
+
return path.join(os.homedir(), '.zshrc');
|
|
17
|
+
if (shell.includes('bash'))
|
|
18
|
+
return path.join(os.homedir(), '.bashrc');
|
|
19
|
+
return path.join(os.homedir(), '.zshrc');
|
|
20
|
+
}
|
|
21
|
+
export function cliEntryPath() {
|
|
22
|
+
if (process.env.CLIPY_CLI_ENTRY)
|
|
23
|
+
return process.env.CLIPY_CLI_ENTRY;
|
|
24
|
+
const here = fileURLToPath(import.meta.url);
|
|
25
|
+
if (here.endsWith('.ts')) {
|
|
26
|
+
return path.resolve(path.dirname(here), '../../dist/cli.js');
|
|
27
|
+
}
|
|
28
|
+
return path.resolve(path.dirname(here), '../cli.js');
|
|
29
|
+
}
|
|
30
|
+
function pathLine(dir) {
|
|
31
|
+
return `export PATH="${dir}:$PATH"`;
|
|
32
|
+
}
|
|
33
|
+
export function pathHasDir(dir, pathEnv = process.env.PATH ?? '') {
|
|
34
|
+
const parts = pathEnv.split(path.delimiter).filter(Boolean);
|
|
35
|
+
const resolved = path.resolve(dir);
|
|
36
|
+
return parts.some((p) => path.resolve(p) === resolved);
|
|
37
|
+
}
|
|
38
|
+
function ensureMarkerBlock(rc, dir) {
|
|
39
|
+
const block = `${MARKER_START}\n${pathLine(dir)}\n${MARKER_END}\n`;
|
|
40
|
+
if (!fs.existsSync(rc)) {
|
|
41
|
+
fs.writeFileSync(rc, `\n${block}`, 'utf8');
|
|
42
|
+
return { written: true, rc };
|
|
43
|
+
}
|
|
44
|
+
const text = fs.readFileSync(rc, 'utf8');
|
|
45
|
+
if (text.includes(MARKER_START))
|
|
46
|
+
return { written: false, rc };
|
|
47
|
+
fs.appendFileSync(rc, `\n${block}`);
|
|
48
|
+
return { written: true, rc };
|
|
49
|
+
}
|
|
50
|
+
function removeMarkerBlock(rc) {
|
|
51
|
+
if (!fs.existsSync(rc))
|
|
52
|
+
return false;
|
|
53
|
+
const text = fs.readFileSync(rc, 'utf8');
|
|
54
|
+
const re = new RegExp(`\\n?${MARKER_START}[\\s\\S]*?${MARKER_END}\\n?`);
|
|
55
|
+
if (!re.test(text))
|
|
56
|
+
return false;
|
|
57
|
+
fs.writeFileSync(rc, text.replace(re, '\n'), 'utf8');
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
export function setup(opts = {}) {
|
|
61
|
+
const dir = binDir();
|
|
62
|
+
const bin = path.join(dir, 'clp');
|
|
63
|
+
if (opts.undo) {
|
|
64
|
+
if (fs.existsSync(bin))
|
|
65
|
+
fs.unlinkSync(bin);
|
|
66
|
+
const removed = removeMarkerBlock(shellRcPath());
|
|
67
|
+
return { bin, target: cliEntryPath(), shim: bin, pathConfigured: pathHasDir(dir), undo: true, hint: removed ? 'Removed PATH block from shell rc' : 'No PATH block found' };
|
|
68
|
+
}
|
|
69
|
+
const target = cliEntryPath();
|
|
70
|
+
if (!fs.existsSync(target)) {
|
|
71
|
+
throw new CliError('SETUP_ERROR', `CLI entry not found at ${target}. Run npm run build first.`, { target }, EXIT.VALIDATION);
|
|
72
|
+
}
|
|
73
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
74
|
+
const shim = `#!/usr/bin/env bash\nexec node ${JSON.stringify(target)} "$@"\n`;
|
|
75
|
+
fs.writeFileSync(bin, shim, { mode: 0o755 });
|
|
76
|
+
let rcUpdated;
|
|
77
|
+
let pathConfigured = pathHasDir(dir);
|
|
78
|
+
if (!pathConfigured) {
|
|
79
|
+
const upd = ensureMarkerBlock(shellRcPath(), dir);
|
|
80
|
+
rcUpdated = upd.rc;
|
|
81
|
+
pathConfigured = true;
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
bin,
|
|
85
|
+
target,
|
|
86
|
+
shim: bin,
|
|
87
|
+
pathConfigured,
|
|
88
|
+
rcUpdated,
|
|
89
|
+
hint: rcUpdated
|
|
90
|
+
? `Added PATH in ${rcUpdated}. Open a new terminal or: source ${rcUpdated}`
|
|
91
|
+
: `clp is at ${bin}`,
|
|
92
|
+
};
|
|
93
|
+
}
|
package/dist/http.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export declare class GoogleClient {
|
|
2
|
+
private readonly getToken;
|
|
3
|
+
private readonly fetchImpl;
|
|
4
|
+
constructor(getToken: () => Promise<string>, fetchImpl?: typeof fetch);
|
|
5
|
+
sheets<T>(method: string, path: string, opts?: {
|
|
6
|
+
query?: Record<string, string | undefined>;
|
|
7
|
+
body?: unknown;
|
|
8
|
+
}): Promise<T>;
|
|
9
|
+
drive<T>(method: string, path: string, opts?: {
|
|
10
|
+
query?: Record<string, string | undefined>;
|
|
11
|
+
body?: unknown;
|
|
12
|
+
}): Promise<T>;
|
|
13
|
+
private request;
|
|
14
|
+
}
|
package/dist/http.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { CliError, EXIT } from './output.js';
|
|
2
|
+
export class GoogleClient {
|
|
3
|
+
getToken;
|
|
4
|
+
fetchImpl;
|
|
5
|
+
constructor(getToken, fetchImpl = fetch) {
|
|
6
|
+
this.getToken = getToken;
|
|
7
|
+
this.fetchImpl = fetchImpl;
|
|
8
|
+
}
|
|
9
|
+
async sheets(method, path, opts = {}) {
|
|
10
|
+
return this.request('https://sheets.googleapis.com/v4', method, path, opts);
|
|
11
|
+
}
|
|
12
|
+
async drive(method, path, opts = {}) {
|
|
13
|
+
return this.request('https://www.googleapis.com/drive/v3', method, path, opts);
|
|
14
|
+
}
|
|
15
|
+
async request(base, method, path, opts) {
|
|
16
|
+
const url = new URL(path.startsWith('http') ? path : `${base}${path}`);
|
|
17
|
+
for (const [k, v] of Object.entries(opts.query ?? {})) {
|
|
18
|
+
if (v !== undefined)
|
|
19
|
+
url.searchParams.set(k, v);
|
|
20
|
+
}
|
|
21
|
+
const token = await this.getToken();
|
|
22
|
+
const res = await this.fetchImpl(url, {
|
|
23
|
+
method,
|
|
24
|
+
headers: {
|
|
25
|
+
Authorization: `Bearer ${token}`,
|
|
26
|
+
'Content-Type': 'application/json',
|
|
27
|
+
},
|
|
28
|
+
body: opts.body !== undefined ? JSON.stringify(opts.body) : undefined,
|
|
29
|
+
});
|
|
30
|
+
const text = await res.text();
|
|
31
|
+
let json = undefined;
|
|
32
|
+
if (text) {
|
|
33
|
+
try {
|
|
34
|
+
json = JSON.parse(text);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
json = { message: text };
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (!res.ok) {
|
|
41
|
+
const apiErr = json;
|
|
42
|
+
const message = apiErr.error?.message || `Google API ${res.status} ${res.statusText}`;
|
|
43
|
+
if (res.status === 401) {
|
|
44
|
+
throw new CliError('AUTH_REQUIRED', 'Run: clp sheet auth login', { status: 401, message }, EXIT.AUTH);
|
|
45
|
+
}
|
|
46
|
+
if (res.status === 403) {
|
|
47
|
+
throw new CliError('PERMISSION_ERROR', message, { status: 403 }, EXIT.PERMISSION);
|
|
48
|
+
}
|
|
49
|
+
throw new CliError('API_ERROR', message, { status: res.status, body: json }, EXIT.API);
|
|
50
|
+
}
|
|
51
|
+
return (json ?? {});
|
|
52
|
+
}
|
|
53
|
+
}
|
package/dist/ids.d.ts
ADDED
package/dist/ids.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { CliError } from './output.js';
|
|
2
|
+
const ID_RE = /\/spreadsheets\/d\/([a-zA-Z0-9-_]+)/;
|
|
3
|
+
export function parseSpreadsheetId(input) {
|
|
4
|
+
const trimmed = input.trim();
|
|
5
|
+
const match = trimmed.match(ID_RE);
|
|
6
|
+
if (match)
|
|
7
|
+
return match[1];
|
|
8
|
+
if (!trimmed)
|
|
9
|
+
throw new CliError('VALIDATION_ERROR', 'Spreadsheet ID or URL is required');
|
|
10
|
+
if (/^[a-zA-Z0-9-_]{20,}$/.test(trimmed) || /^[a-zA-Z0-9-_]+$/.test(trimmed)) {
|
|
11
|
+
return trimmed;
|
|
12
|
+
}
|
|
13
|
+
throw new CliError('VALIDATION_ERROR', `Could not parse spreadsheet ID from: ${input}`);
|
|
14
|
+
}
|
|
15
|
+
export function resolveSpreadsheetId(positional, flag) {
|
|
16
|
+
const raw = positional || flag;
|
|
17
|
+
if (!raw) {
|
|
18
|
+
throw new CliError('VALIDATION_ERROR', 'Pass a spreadsheet ID/URL as the argument or --spreadsheet');
|
|
19
|
+
}
|
|
20
|
+
return parseSpreadsheetId(raw);
|
|
21
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare function parseJsonInput(raw: string, label: string): unknown;
|
|
2
|
+
export declare function asObject(value: unknown, label: string): Record<string, unknown>;
|
|
3
|
+
export declare function asObjectArray(value: unknown, label: string): Record<string, unknown>[];
|
|
4
|
+
export declare function asGrid(value: unknown, label: string): unknown[][];
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { CliError } from './output.js';
|
|
3
|
+
export function parseJsonInput(raw, label) {
|
|
4
|
+
const text = raw.startsWith('@')
|
|
5
|
+
? fs.readFileSync(raw.slice(1), 'utf8')
|
|
6
|
+
: raw;
|
|
7
|
+
try {
|
|
8
|
+
return JSON.parse(text);
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
throw new CliError('VALIDATION_ERROR', `${label} is not valid JSON${raw.startsWith('@') ? ` (file ${raw.slice(1)})` : ''}`);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export function asObject(value, label) {
|
|
15
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
16
|
+
throw new CliError('VALIDATION_ERROR', `${label} must be a JSON object`);
|
|
17
|
+
}
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
20
|
+
export function asObjectArray(value, label) {
|
|
21
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
22
|
+
return [value];
|
|
23
|
+
}
|
|
24
|
+
if (!Array.isArray(value) || value.some((row) => !row || typeof row !== 'object' || Array.isArray(row))) {
|
|
25
|
+
throw new CliError('VALIDATION_ERROR', `${label} must be a JSON object or array of objects`);
|
|
26
|
+
}
|
|
27
|
+
return value;
|
|
28
|
+
}
|
|
29
|
+
export function asGrid(value, label) {
|
|
30
|
+
if (!Array.isArray(value) || value.some((row) => !Array.isArray(row))) {
|
|
31
|
+
throw new CliError('VALIDATION_ERROR', `${label} must be a JSON 2D array`);
|
|
32
|
+
}
|
|
33
|
+
return value;
|
|
34
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { GridRange } from '../ranges.js';
|
|
2
|
+
export type FormatFlags = {
|
|
3
|
+
bold?: boolean;
|
|
4
|
+
italic?: boolean;
|
|
5
|
+
bg?: string;
|
|
6
|
+
fg?: string;
|
|
7
|
+
align?: string;
|
|
8
|
+
valign?: string;
|
|
9
|
+
pattern?: string;
|
|
10
|
+
wrap?: boolean;
|
|
11
|
+
};
|
|
12
|
+
export declare function formatFields(flags: FormatFlags): string[];
|
|
13
|
+
export declare function formatToRepeatCell(range: GridRange, flags: FormatFlags): Record<string, unknown>;
|
|
14
|
+
export declare function freezeRequest(sheetId: number, rows?: number, columns?: number): Record<string, unknown>;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { parseColor } from '../colors.js';
|
|
2
|
+
export function formatFields(flags) {
|
|
3
|
+
const fields = [];
|
|
4
|
+
if (flags.bold !== undefined || flags.italic !== undefined || flags.fg) {
|
|
5
|
+
const parts = ['textFormat'];
|
|
6
|
+
if (flags.bold !== undefined)
|
|
7
|
+
parts.push('textFormat.bold');
|
|
8
|
+
if (flags.italic !== undefined)
|
|
9
|
+
parts.push('textFormat.italic');
|
|
10
|
+
if (flags.fg)
|
|
11
|
+
parts.push('textFormat.foregroundColor');
|
|
12
|
+
fields.push(...parts);
|
|
13
|
+
}
|
|
14
|
+
if (flags.bg)
|
|
15
|
+
fields.push('backgroundColor');
|
|
16
|
+
if (flags.align)
|
|
17
|
+
fields.push('horizontalAlignment');
|
|
18
|
+
if (flags.valign)
|
|
19
|
+
fields.push('verticalAlignment');
|
|
20
|
+
if (flags.pattern)
|
|
21
|
+
fields.push('numberFormat');
|
|
22
|
+
if (flags.wrap !== undefined)
|
|
23
|
+
fields.push('wrapStrategy');
|
|
24
|
+
return fields;
|
|
25
|
+
}
|
|
26
|
+
export function formatToRepeatCell(range, flags) {
|
|
27
|
+
const fmt = {};
|
|
28
|
+
if (flags.bold !== undefined || flags.italic !== undefined || flags.fg) {
|
|
29
|
+
fmt.textFormat = {};
|
|
30
|
+
if (flags.bold !== undefined)
|
|
31
|
+
fmt.textFormat.bold = flags.bold;
|
|
32
|
+
if (flags.italic !== undefined)
|
|
33
|
+
fmt.textFormat.italic = flags.italic;
|
|
34
|
+
if (flags.fg)
|
|
35
|
+
fmt.textFormat.foregroundColor = parseColor(flags.fg);
|
|
36
|
+
}
|
|
37
|
+
if (flags.bg)
|
|
38
|
+
fmt.backgroundColor = parseColor(flags.bg);
|
|
39
|
+
if (flags.align)
|
|
40
|
+
fmt.horizontalAlignment = flags.align.toUpperCase();
|
|
41
|
+
if (flags.valign)
|
|
42
|
+
fmt.verticalAlignment = flags.valign.toUpperCase();
|
|
43
|
+
if (flags.pattern)
|
|
44
|
+
fmt.numberFormat = { type: 'NUMBER', pattern: flags.pattern };
|
|
45
|
+
if (flags.wrap !== undefined)
|
|
46
|
+
fmt.wrapStrategy = flags.wrap ? 'WRAP' : 'OVERFLOW_CELL';
|
|
47
|
+
const fieldList = formatFields(flags).map((f) => `userEnteredFormat.${f}`);
|
|
48
|
+
return {
|
|
49
|
+
repeatCell: {
|
|
50
|
+
range,
|
|
51
|
+
cell: { userEnteredFormat: fmt },
|
|
52
|
+
fields: fieldList.join(','),
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
export function freezeRequest(sheetId, rows, columns) {
|
|
57
|
+
const gridProperties = {};
|
|
58
|
+
const fields = [];
|
|
59
|
+
if (rows !== undefined) {
|
|
60
|
+
gridProperties.frozenRowCount = rows;
|
|
61
|
+
fields.push('gridProperties.frozenRowCount');
|
|
62
|
+
}
|
|
63
|
+
if (columns !== undefined) {
|
|
64
|
+
gridProperties.frozenColumnCount = columns;
|
|
65
|
+
fields.push('gridProperties.frozenColumnCount');
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
updateSheetProperties: {
|
|
69
|
+
properties: { sheetId, gridProperties },
|
|
70
|
+
fields: fields.join(','),
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { parseSpreadsheetId } from '../ids.js';
|
|
2
|
+
export { GoogleClient } from '../http.js';
|
|
3
|
+
export { createOps } from './ops.js';
|
|
4
|
+
export { valuesToTable, objectToRow } from './table.js';
|
|
5
|
+
export { formatToRepeatCell, freezeRequest } from './format.js';
|
|
6
|
+
export { getAccessToken, login, logout, status } from '../auth.js';
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { parseSpreadsheetId } from '../ids.js';
|
|
2
|
+
export { GoogleClient } from '../http.js';
|
|
3
|
+
export { createOps } from './ops.js';
|
|
4
|
+
export { valuesToTable, objectToRow } from './table.js';
|
|
5
|
+
export { formatToRepeatCell, freezeRequest } from './format.js';
|
|
6
|
+
export { getAccessToken, login, logout, status } from '../auth.js';
|