incwo-cli 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/.gitleaks.toml +15 -0
- package/.husky/pre-commit +26 -0
- package/README.md +179 -0
- package/dist/api.d.ts +11 -0
- package/dist/api.js +87 -0
- package/dist/commands/generic.d.ts +3 -0
- package/dist/commands/generic.js +163 -0
- package/dist/config.d.ts +10 -0
- package/dist/config.js +144 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +29 -0
- package/dist/resources.d.ts +22 -0
- package/dist/resources.js +497 -0
- package/dist/ui.d.ts +9 -0
- package/dist/ui.js +58 -0
- package/package.json +33 -0
- package/src/api.ts +93 -0
- package/src/commands/generic.ts +156 -0
- package/src/config.ts +118 -0
- package/src/index.ts +33 -0
- package/src/resources.ts +560 -0
- package/src/ui.ts +57 -0
- package/tsconfig.json +17 -0
package/src/api.ts
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import axios, { AxiosInstance } from 'axios';
|
|
2
|
+
import { parseStringPromise, Builder } from 'xml2js';
|
|
3
|
+
import { IncwoConfig, baseUrl } from './config';
|
|
4
|
+
|
|
5
|
+
export class IncwoClient {
|
|
6
|
+
private http: AxiosInstance;
|
|
7
|
+
private base: string;
|
|
8
|
+
|
|
9
|
+
constructor(config: IncwoConfig) {
|
|
10
|
+
this.base = baseUrl(config);
|
|
11
|
+
this.http = axios.create({
|
|
12
|
+
baseURL: this.base,
|
|
13
|
+
auth: { username: config.login, password: config.password },
|
|
14
|
+
headers: { 'Content-Type': 'application/xml', 'Accept': 'application/xml' },
|
|
15
|
+
timeout: 15000,
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// GET /{resource}.xml with optional query params
|
|
20
|
+
async list(resource: string, params: Record<string, string> = {}): Promise<any[]> {
|
|
21
|
+
const response = await this.http.get(`/${resource}.xml`, { params });
|
|
22
|
+
return parseXmlList(response.data);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// GET /{resource}/{id}.xml
|
|
26
|
+
async get(resource: string, id: string): Promise<any> {
|
|
27
|
+
const response = await this.http.get(`/${resource}/${id}.xml`);
|
|
28
|
+
const parsed = await parseXmlList(response.data);
|
|
29
|
+
return parsed[0] ?? null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// POST /{resource}.xml — accepts a plain JS object, converts to XML
|
|
33
|
+
async create(resource: string, data: Record<string, any>): Promise<any> {
|
|
34
|
+
const singularResource = toSingular(resource);
|
|
35
|
+
const xml = jsonToXml(singularResource, data);
|
|
36
|
+
const response = await this.http.post(`/${resource}.xml`, xml);
|
|
37
|
+
const parsed = await parseXmlList(response.data);
|
|
38
|
+
return parsed[0] ?? null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// PUT /{resource}/{id}.xml — accepts a plain JS object, converts to XML
|
|
42
|
+
async update(resource: string, id: string, data: Record<string, any>): Promise<any> {
|
|
43
|
+
const singularResource = toSingular(resource);
|
|
44
|
+
const xml = jsonToXml(singularResource, data);
|
|
45
|
+
const response = await this.http.put(`/${resource}/${id}.xml`, xml);
|
|
46
|
+
const parsed = await parseXmlList(response.data);
|
|
47
|
+
return parsed[0] ?? null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// DELETE /{resource}/{id}.xml
|
|
51
|
+
async destroy(resource: string, id: string): Promise<void> {
|
|
52
|
+
await this.http.delete(`/${resource}/${id}.xml`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Converts a flat JS object to an XML string with the given root element
|
|
57
|
+
// e.g. jsonToXml('contact', { first_name: 'Bob' }) → <contact><first_name>Bob</first_name></contact>
|
|
58
|
+
function jsonToXml(rootElement: string, data: Record<string, any>): string {
|
|
59
|
+
const builder = new Builder({ rootName: rootElement, headless: true });
|
|
60
|
+
return builder.buildObject(data);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Naive singularization: bill_sheets → bill_sheet, contacts → contact
|
|
64
|
+
function toSingular(resource: string): string {
|
|
65
|
+
if (resource.endsWith('ies')) return resource.slice(0, -3) + 'y';
|
|
66
|
+
if (resource.endsWith('s')) return resource.slice(0, -1);
|
|
67
|
+
return resource;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function parseXmlList(xml: string): Promise<any[]> {
|
|
71
|
+
const parsed = await parseStringPromise(xml, {
|
|
72
|
+
explicitArray: false,
|
|
73
|
+
ignoreAttrs: true,
|
|
74
|
+
trim: true,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
if (!parsed) return [];
|
|
78
|
+
|
|
79
|
+
const rootKey = Object.keys(parsed)[0];
|
|
80
|
+
const root = parsed[rootKey];
|
|
81
|
+
|
|
82
|
+
if (!root) return [];
|
|
83
|
+
|
|
84
|
+
const childKey = Object.keys(root)[0];
|
|
85
|
+
if (childKey && Array.isArray(root[childKey])) {
|
|
86
|
+
return root[childKey];
|
|
87
|
+
}
|
|
88
|
+
if (childKey && typeof root[childKey] === 'object') {
|
|
89
|
+
return [root[childKey]];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return [root];
|
|
93
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import ora from 'ora';
|
|
3
|
+
import { loadConfig } from '../config';
|
|
4
|
+
import { IncwoClient } from '../api';
|
|
5
|
+
import { printTable, printObject, error, success } from '../ui';
|
|
6
|
+
import { ResourceDef } from '../resources';
|
|
7
|
+
|
|
8
|
+
export function makeCommand(res: ResourceDef): Command {
|
|
9
|
+
const cmd = new Command(res.command).description(res.description);
|
|
10
|
+
|
|
11
|
+
// ── list ──────────────────────────────────────────────────────────────────
|
|
12
|
+
const listCmd = cmd
|
|
13
|
+
.command('list')
|
|
14
|
+
.description(`List ${res.description.toLowerCase()}`)
|
|
15
|
+
.option('--page <n>', 'Page number', '1')
|
|
16
|
+
.option('--search <q>', 'Full-text search')
|
|
17
|
+
.option('--filter <json>', 'ufilters JSON (e.g. \'{"status":{"eq":"active"}}\')');
|
|
18
|
+
|
|
19
|
+
if (res.hasSheetType) {
|
|
20
|
+
listCmd.option('--sheet-type <type>', 'Filter by sheet type (proposal, sale_order, delivery_note, purchase_order…)');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (res.hasDateFilter) {
|
|
24
|
+
listCmd
|
|
25
|
+
.option('--from <date>', 'Start date (YYYY-MM-DD)')
|
|
26
|
+
.option('--to <date>', 'End date (YYYY-MM-DD)');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
listCmd.action(async (opts) => {
|
|
30
|
+
const config = loadConfig();
|
|
31
|
+
if (!config) return error('No configuration found. Run: incwo config');
|
|
32
|
+
|
|
33
|
+
const spinner = ora('Loading…').start();
|
|
34
|
+
try {
|
|
35
|
+
const params: Record<string, string> = {
|
|
36
|
+
page: opts.page,
|
|
37
|
+
...res.defaultParams,
|
|
38
|
+
};
|
|
39
|
+
if (opts.search) params['search'] = opts.search;
|
|
40
|
+
if (opts.filter) params['ufilters'] = opts.filter;
|
|
41
|
+
if (opts.sheetType) params['sheet_type'] = opts.sheetType;
|
|
42
|
+
if (opts.from) params['filter_from_date'] = opts.from;
|
|
43
|
+
if (opts.to) params['filter_to_date'] = opts.to;
|
|
44
|
+
|
|
45
|
+
const client = new IncwoClient(config);
|
|
46
|
+
const rows = await client.list(res.endpoint, params);
|
|
47
|
+
spinner.stop();
|
|
48
|
+
printTable(rows, res.columns);
|
|
49
|
+
} catch (e: any) {
|
|
50
|
+
spinner.stop();
|
|
51
|
+
error(e.response?.data ?? e.message);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// ── get ───────────────────────────────────────────────────────────────────
|
|
56
|
+
cmd
|
|
57
|
+
.command('get <id>')
|
|
58
|
+
.description(`Show a ${res.description.toLowerCase()} by ID`)
|
|
59
|
+
.action(async (id) => {
|
|
60
|
+
const config = loadConfig();
|
|
61
|
+
if (!config) return error('No configuration found. Run: incwo config');
|
|
62
|
+
|
|
63
|
+
const spinner = ora('Loading…').start();
|
|
64
|
+
try {
|
|
65
|
+
const client = new IncwoClient(config);
|
|
66
|
+
const obj = await client.get(res.endpoint, id);
|
|
67
|
+
spinner.stop();
|
|
68
|
+
if (!obj) return error(`${res.command} #${id} not found`);
|
|
69
|
+
printObject(obj, `${res.command} #${id}`);
|
|
70
|
+
} catch (e: any) {
|
|
71
|
+
spinner.stop();
|
|
72
|
+
error(e.response?.data ?? e.message);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// ── create ────────────────────────────────────────────────────────────────
|
|
77
|
+
cmd
|
|
78
|
+
.command('create')
|
|
79
|
+
.description(`Create a new ${res.description.toLowerCase()}`)
|
|
80
|
+
.requiredOption('--data <json>', 'Fields as JSON (e.g. \'{"first_name":"Bob","last_name":"Smith"}\')')
|
|
81
|
+
.action(async (opts) => {
|
|
82
|
+
const config = loadConfig();
|
|
83
|
+
if (!config) return error('No configuration found. Run: incwo config');
|
|
84
|
+
|
|
85
|
+
let data: Record<string, any>;
|
|
86
|
+
try {
|
|
87
|
+
data = JSON.parse(opts.data);
|
|
88
|
+
} catch {
|
|
89
|
+
return error('Invalid JSON in --data');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const spinner = ora('Creating…').start();
|
|
93
|
+
try {
|
|
94
|
+
const client = new IncwoClient(config);
|
|
95
|
+
const obj = await client.create(res.endpoint, data);
|
|
96
|
+
spinner.stop();
|
|
97
|
+
if (!obj) return error('Created but no response returned');
|
|
98
|
+
success(`${res.command} created (id: ${obj.id ?? '?'})`);
|
|
99
|
+
printObject(obj);
|
|
100
|
+
} catch (e: any) {
|
|
101
|
+
spinner.stop();
|
|
102
|
+
error(e.response?.data ?? e.message);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// ── update ────────────────────────────────────────────────────────────────
|
|
107
|
+
cmd
|
|
108
|
+
.command('update <id>')
|
|
109
|
+
.description(`Update a ${res.description.toLowerCase()}`)
|
|
110
|
+
.requiredOption('--data <json>', 'Fields to update as JSON (e.g. \'{"first_name":"Bob"}\')')
|
|
111
|
+
.action(async (id, opts) => {
|
|
112
|
+
const config = loadConfig();
|
|
113
|
+
if (!config) return error('No configuration found. Run: incwo config');
|
|
114
|
+
|
|
115
|
+
let data: Record<string, any>;
|
|
116
|
+
try {
|
|
117
|
+
data = JSON.parse(opts.data);
|
|
118
|
+
} catch {
|
|
119
|
+
return error('Invalid JSON in --data');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const spinner = ora('Updating…').start();
|
|
123
|
+
try {
|
|
124
|
+
const client = new IncwoClient(config);
|
|
125
|
+
const obj = await client.update(res.endpoint, id, data);
|
|
126
|
+
spinner.stop();
|
|
127
|
+
success(`${res.command} #${id} updated`);
|
|
128
|
+
if (obj) printObject(obj);
|
|
129
|
+
} catch (e: any) {
|
|
130
|
+
spinner.stop();
|
|
131
|
+
error(e.response?.data ?? e.message);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// ── delete ────────────────────────────────────────────────────────────────
|
|
136
|
+
cmd
|
|
137
|
+
.command('delete <id>')
|
|
138
|
+
.description(`Delete a ${res.description.toLowerCase()}`)
|
|
139
|
+
.action(async (id) => {
|
|
140
|
+
const config = loadConfig();
|
|
141
|
+
if (!config) return error('No configuration found. Run: incwo config');
|
|
142
|
+
|
|
143
|
+
const spinner = ora('Deleting…').start();
|
|
144
|
+
try {
|
|
145
|
+
const client = new IncwoClient(config);
|
|
146
|
+
await client.destroy(res.endpoint, id);
|
|
147
|
+
spinner.stop();
|
|
148
|
+
success(`${res.command} #${id} deleted`);
|
|
149
|
+
} catch (e: any) {
|
|
150
|
+
spinner.stop();
|
|
151
|
+
error(e.response?.data ?? e.message);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
return cmd;
|
|
156
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as os from 'os';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import inquirer from 'inquirer';
|
|
5
|
+
|
|
6
|
+
export interface IncwoConfig {
|
|
7
|
+
server: string; // e.g. "mycompany.incwo.com" or "www.incwo.com"
|
|
8
|
+
business_file_id: string;
|
|
9
|
+
login: string;
|
|
10
|
+
password: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const CONFIG_DIR = path.join(os.homedir(), '.incwo');
|
|
14
|
+
const CONFIG_PATH = path.join(CONFIG_DIR, 'config.json');
|
|
15
|
+
|
|
16
|
+
export function loadConfig(): IncwoConfig | null {
|
|
17
|
+
if (!fs.existsSync(CONFIG_PATH)) return null;
|
|
18
|
+
try {
|
|
19
|
+
const raw = fs.readFileSync(CONFIG_PATH, 'utf-8');
|
|
20
|
+
return JSON.parse(raw) as IncwoConfig;
|
|
21
|
+
} catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function saveConfig(config: IncwoConfig): void {
|
|
27
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
28
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
29
|
+
}
|
|
30
|
+
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), 'utf-8');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function baseUrl(config: IncwoConfig): string {
|
|
34
|
+
return `https://${config.server}/${config.business_file_id}`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function runConfigWizard(): Promise<void> {
|
|
38
|
+
const chalk = (await import('chalk')).default;
|
|
39
|
+
|
|
40
|
+
console.log(chalk.bold('\nincwo CLI — Configuration\n'));
|
|
41
|
+
|
|
42
|
+
const { inputMode } = await inquirer.prompt([
|
|
43
|
+
{
|
|
44
|
+
type: 'list',
|
|
45
|
+
name: 'inputMode',
|
|
46
|
+
message: 'How would you like to configure the server?',
|
|
47
|
+
choices: [
|
|
48
|
+
{ name: 'Enter the full endpoint URL (e.g. mycompany.incwo.com/12345)', value: 'url' },
|
|
49
|
+
{ name: 'Enter the shard and business file ID separately', value: 'manual' },
|
|
50
|
+
],
|
|
51
|
+
},
|
|
52
|
+
]);
|
|
53
|
+
|
|
54
|
+
let server: string;
|
|
55
|
+
let business_file_id: string;
|
|
56
|
+
|
|
57
|
+
if (inputMode === 'url') {
|
|
58
|
+
const { endpoint } = await inquirer.prompt([
|
|
59
|
+
{
|
|
60
|
+
type: 'input',
|
|
61
|
+
name: 'endpoint',
|
|
62
|
+
message: 'Endpoint URL (e.g. mycompany.incwo.com/12345 or www.incwo.com/12345):',
|
|
63
|
+
validate: (v: string) => {
|
|
64
|
+
const clean = v.replace(/^https?:\/\//, '').replace(/\/$/, '');
|
|
65
|
+
const parts = clean.split('/');
|
|
66
|
+
if (parts.length < 2 || !parts[0] || !parts[1]) {
|
|
67
|
+
return 'Expected format: shard.incwo.com/business_file_id';
|
|
68
|
+
}
|
|
69
|
+
return true;
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
]);
|
|
73
|
+
const clean = endpoint.replace(/^https?:\/\//, '').replace(/\/$/, '');
|
|
74
|
+
const slashIdx = clean.indexOf('/');
|
|
75
|
+
server = clean.substring(0, slashIdx);
|
|
76
|
+
business_file_id = clean.substring(slashIdx + 1).split('/')[0];
|
|
77
|
+
} else {
|
|
78
|
+
const answers = await inquirer.prompt([
|
|
79
|
+
{
|
|
80
|
+
type: 'input',
|
|
81
|
+
name: 'shard',
|
|
82
|
+
message: 'Shard (leave empty if no shard → www.incwo.com):',
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
type: 'input',
|
|
86
|
+
name: 'business_file_id',
|
|
87
|
+
message: 'Business file ID:',
|
|
88
|
+
validate: (v: string) => v.trim() !== '' || 'Required',
|
|
89
|
+
},
|
|
90
|
+
]);
|
|
91
|
+
server = answers.shard.trim()
|
|
92
|
+
? `${answers.shard.trim()}.incwo.com`
|
|
93
|
+
: 'www.incwo.com';
|
|
94
|
+
business_file_id = answers.business_file_id.trim();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const { login, password } = await inquirer.prompt([
|
|
98
|
+
{
|
|
99
|
+
type: 'input',
|
|
100
|
+
name: 'login',
|
|
101
|
+
message: 'Login:',
|
|
102
|
+
validate: (v: string) => v.trim() !== '' || 'Required',
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
type: 'password',
|
|
106
|
+
name: 'password',
|
|
107
|
+
message: 'Password:',
|
|
108
|
+
mask: '*',
|
|
109
|
+
validate: (v: string) => v.trim() !== '' || 'Required',
|
|
110
|
+
},
|
|
111
|
+
]);
|
|
112
|
+
|
|
113
|
+
const config: IncwoConfig = { server, business_file_id, login, password };
|
|
114
|
+
saveConfig(config);
|
|
115
|
+
|
|
116
|
+
console.log(chalk.green(`\n✓ Configuration saved to ${CONFIG_PATH}`));
|
|
117
|
+
console.log(chalk.dim(` Endpoint: https://${server}/${business_file_id}\n`));
|
|
118
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import { printBanner } from './ui';
|
|
4
|
+
import { runConfigWizard } from './config';
|
|
5
|
+
import { makeCommand } from './commands/generic';
|
|
6
|
+
import { RESOURCES } from './resources';
|
|
7
|
+
|
|
8
|
+
const program = new Command();
|
|
9
|
+
|
|
10
|
+
program
|
|
11
|
+
.name('incwo')
|
|
12
|
+
.description('CLI for incwo CRM/ERP')
|
|
13
|
+
.version('0.1.0');
|
|
14
|
+
|
|
15
|
+
// Banner sur --help global ou sans arguments
|
|
16
|
+
if (process.argv.length <= 2 || process.argv[2] === '--help' || process.argv[2] === '-h') {
|
|
17
|
+
printBanner();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// incwo config
|
|
21
|
+
program
|
|
22
|
+
.command('config')
|
|
23
|
+
.description('Configure incwo access (server, credentials)')
|
|
24
|
+
.action(async () => {
|
|
25
|
+
await runConfigWizard();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// Toutes les ressources via le factory générique
|
|
29
|
+
for (const resource of RESOURCES) {
|
|
30
|
+
program.addCommand(makeCommand(resource));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
program.parse(process.argv);
|