proca 2.3.1 → 2.5.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/README.md CHANGED
@@ -48,6 +48,7 @@ you should also use the local proca-api in your [widget generator](https://githu
48
48
 
49
49
  <!-- commands -->
50
50
  * [`proca action add`](#proca-action-add)
51
+ * [`proca action add mtt`](#proca-action-add-mtt)
51
52
  * [`proca action confirm`](#proca-action-confirm)
52
53
  * [`proca action count`](#proca-action-count)
53
54
  * [`proca action list [TITLE]`](#proca-action-list-title)
@@ -111,6 +112,7 @@ you should also use the local proca-api in your [widget generator](https://githu
111
112
  * [`proca widget add`](#proca-widget-add)
112
113
  * [`proca widget delete`](#proca-widget-delete)
113
114
  * [`proca widget external`](#proca-widget-external)
115
+ * [`proca widget external cron`](#proca-widget-external-cron)
114
116
  * [`proca widget get`](#proca-widget-get)
115
117
  * [`proca widget list`](#proca-widget-list)
116
118
  * [`proca widget rebuild`](#proca-widget-rebuild)
@@ -162,6 +164,50 @@ EXAMPLES
162
164
  $ proca action add -i <widget_id> --firstname=John --email=john@example.org target=715a9580-cfe6-4005-9e23-61a62ddecfea --subject='MTT subject' --body='message MTT'
163
165
  ```
164
166
 
167
+ ## `proca action add mtt`
168
+
169
+ ```
170
+ USAGE
171
+ $ proca action add mtt [ID_NAME_DXID...] -i <value> --firstname <value> --email <value>
172
+ [--json | --csv | --markdown] [--env <value>] [--simplify] [-n <the_short_name>] [-x <value>] [--testing] [--optin]
173
+ [--action_type <value>] [--lastname <value>] [--street <value>] [--locality <value>] [--region <value>] [--country
174
+ <value>] [--utm <value>] [--target <value>] [--subject <value>] [--body <value>]
175
+
176
+ FLAGS
177
+ -i, --id=<value> (required) widget's id
178
+ -n, --name=<the_short_name> name (technical short name, also called slug)
179
+ -x, --dxid=<value> dxid
180
+ --action_type=<value> [default: email]
181
+ --body=<value> [mtt] body of the email
182
+ --country=<value> 2-letter country iso code
183
+ --email=<value> (required) email
184
+ --env=<value> [default: default] allow to switch between configurations (server or users)
185
+ --firstname=<value> (required) supporter's firstname
186
+ --lastname=<value>
187
+ --locality=<value>
188
+ --[no-]optin Whether the user opts in (default: false). Use --optin to enable or --no-optin to
189
+ explicitly disable.
190
+ --region=<value>
191
+ --street=<value>
192
+ --subject=<value> [mtt] subject of the email
193
+ --target=<value> [mtt] uid of the target
194
+ --[no-]testing Run action in testing mode (default: true). Use --no-testing to disable.
195
+ --utm=<value> utm=campaign.source.medium
196
+
197
+ OUTPUT FLAGS
198
+ --csv Format output as csv
199
+ --json Format output as json
200
+ --markdown Format output as markdown table
201
+ --[no-]simplify flatten and filter to output only the most important attributes, mostly relevant for json
202
+
203
+ EXAMPLES
204
+ $ proca action add mtt -i <widget_id> --firstname=John --email=john@example.org
205
+
206
+ $ proca action add mtt -i <widget_id> --firstname=John --email=john@example.org --country=FR custom1=A custom2=B
207
+
208
+ $ proca action add mtt -i <widget_id> --firstname=John --email=john@example.org target=715a9580-cfe6-4005-9e23-61a62ddecfea --subject='MTT subject' --body='message MTT'
209
+ ```
210
+
165
211
  ## `proca action confirm`
166
212
 
167
213
  Should the supporter confirm the action? it can be set either for all the widgets or an organisation or all the widgets of a campaign
@@ -470,8 +516,8 @@ list all the campaigns
470
516
 
471
517
  ```
472
518
  USAGE
473
- $ proca campaign list [--json | --csv | --markdown] [--env <value>] [--simplify] [-n
474
- <name of the organisation>] [-t <campaign title>...] [--stats]
519
+ $ proca campaign list [NAME] [--json | --csv | --markdown] [--env <value>] [--simplify]
520
+ [-n <name of the organisation>] [-t <campaign title>...] [--stats]
475
521
 
476
522
  FLAGS
477
523
  -n, --name=<name of the organisation> name (technical short name, also called slug)
@@ -1222,7 +1268,7 @@ EXAMPLES
1222
1268
  $ proca org logo <name of the ngo>
1223
1269
  ```
1224
1270
 
1225
- _See code: [src/commands/org/logo.ts](https://github.com/fixthestatusquo/proca-cli/blob/v2.3.1/src/commands/org/logo.ts)_
1271
+ _See code: [src/commands/org/logo.ts](https://github.com/fixthestatusquo/proca-cli/blob/v2.5.0/src/commands/org/logo.ts)_
1226
1272
 
1227
1273
  ## `proca org user get`
1228
1274
 
@@ -1545,7 +1591,7 @@ _See code: [@oclif/plugin-plugins](https://github.com/oclif/plugin-plugins/blob/
1545
1591
 
1546
1592
  ## `proca service add`
1547
1593
 
1548
- Set service, usually email backend for an org, the specific meaning of each param is dependant on the service
1594
+ Set service, usually email backend for an org. the specific meaning of each param is dependant on the service.
1549
1595
 
1550
1596
  ```
1551
1597
  USAGE
@@ -1559,7 +1605,7 @@ FLAGS
1559
1605
  --host=<value> server of the service
1560
1606
  --password=<value> credential of the account on the service
1561
1607
  --path=<value> path on the service
1562
- --type=<option> (required) [default: system] type of the service
1608
+ --type=<option> (required) type of the service
1563
1609
  <options: mailjet|ses|stripe|test_stripe|preview|webhook|supabase|smtp>
1564
1610
  --user=<value> credential of the account on the service
1565
1611
 
@@ -1570,7 +1616,15 @@ OUTPUT FLAGS
1570
1616
  --[no-]simplify flatten and filter to output only the most important attributes, mostly relevant for json
1571
1617
 
1572
1618
  DESCRIPTION
1573
- Set service, usually email backend for an org, the specific meaning of each param is dependant on the service
1619
+ Set service, usually email backend for an org. the specific meaning of each param is dependant on the service.
1620
+ If a service from that type exists, it will replace it
1621
+
1622
+ EXAMPLES
1623
+ $ proca service add -o example_org --type system
1624
+
1625
+ $ proca service add -o example_org --host=tls://mail.example.org:587 --user=login --password "secret" --type smtp
1626
+
1627
+ $ proca service add -o example_org --host=ssl://mail.example.org:465 --user=login --password "secret" --type smtp
1574
1628
  ```
1575
1629
 
1576
1630
  ## `proca service list`
@@ -1970,6 +2024,34 @@ EXAMPLES
1970
2024
  $ proca widget external --url https://mitmachen.wwf.de/node/506/polling
1971
2025
  ```
1972
2026
 
2027
+ ## `proca widget external cron`
2028
+
2029
+ Pull all external counters and save it into a widget extra Supporter. symlink the widget json into config/counter
2030
+
2031
+ ```
2032
+ USAGE
2033
+ $ proca widget external cron [--json | --csv | --markdown] [--env <value>] [--simplify]
2034
+ [--dry-run]
2035
+
2036
+ FLAGS
2037
+ --dry-run just fetch, don't update
2038
+ --env=<value> [default: default] allow to switch between configurations (server or users)
2039
+
2040
+ OUTPUT FLAGS
2041
+ --csv Format output as csv
2042
+ --json Format output as json
2043
+ --markdown Format output as markdown table
2044
+ --[no-]simplify flatten and filter to output only the most important attributes, mostly relevant for json
2045
+
2046
+ DESCRIPTION
2047
+ Pull all external counters and save it into a widget extra Supporter. symlink the widget json into config/counter
2048
+
2049
+ EXAMPLES
2050
+ $ proca widget external cron
2051
+ ```
2052
+
2053
+ _See code: [src/commands/widget/external/cron.ts](https://github.com/fixthestatusquo/proca-cli/blob/v2.5.0/src/commands/widget/external/cron.ts)_
2054
+
1973
2055
  ## `proca widget get`
1974
2056
 
1975
2057
  view a widget
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "proca",
3
3
  "description": "Access the proca api",
4
- "version": "2.3.1",
4
+ "version": "2.5.0",
5
5
  "author": "Xavier",
6
6
  "bin": {
7
7
  "proca": "proca-cli"
@@ -21,6 +21,7 @@
21
21
  "merge-anything": "^6.0.6",
22
22
  "object-path": "^0.11.8",
23
23
  "prompts": "^2.4.2",
24
+ "simple-git": "^3.33.0",
24
25
  "typescript": "^5.7.3",
25
26
  "urql": "^4.1.0"
26
27
  },
@@ -0,0 +1,192 @@
1
+ import { Args, Flags } from "@oclif/core";
2
+ import { error, stdout, ux } from "@oclif/core/ux";
3
+ import Command from "#src/procaCommand.mjs";
4
+ import { gql, mutation } from "#src/urql.mjs";
5
+ import { getTwitter } from "#src/util/twitter.mjs";
6
+
7
+ export default class ActionAdd extends Command {
8
+ static examples = [
9
+ "<%= config.bin %> <%= command.id %> -i <widget_id> --firstname=John --email=john@example.org",
10
+ "<%= config.bin %> <%= command.id %> -i <widget_id> --firstname=John --email=john@example.org --country=FR custom1=A custom2=B",
11
+ "<%= config.bin %> <%= command.id %> -i <widget_id> --firstname=John --email=john@example.org target=715a9580-cfe6-4005-9e23-61a62ddecfea --subject='MTT subject' --body='message MTT'",
12
+ ];
13
+
14
+ static args = this.multiid();
15
+ static strict = false; //THIS DOES NOT WORK
16
+
17
+ static flags = {
18
+ ...this.flagify({ multiid: true }),
19
+ id: Flags.integer({
20
+ char: "i",
21
+ description: "widget's id",
22
+ required: true,
23
+ }),
24
+ testing: Flags.boolean({
25
+ default: true,
26
+ allowNo: true, // ✅ enables --no-testing
27
+ description:
28
+ "Run action in testing mode (default: true). Use --no-testing to disable.",
29
+ }),
30
+ optin: Flags.boolean({
31
+ default: false,
32
+ allowNo: true, // ✅ enables --no-optin
33
+ description:
34
+ "Whether the user opts in (default: false). Use --optin to enable or --no-optin to explicitly disable.",
35
+ }),
36
+ action_type: Flags.string({
37
+ default: "email",
38
+ }),
39
+ firstname: Flags.string({
40
+ description: "supporter's firstname",
41
+ required: true,
42
+ }),
43
+ lastname: Flags.string(),
44
+ street: Flags.string(),
45
+ locality: Flags.string(),
46
+ region: Flags.string(),
47
+ country: Flags.string({
48
+ description: "2-letter country iso code",
49
+ parse: async (input) => {
50
+ if (input && !/^[A-Za-z]{2}$/.test(input)) {
51
+ throw new Error("Country code must be exactly 2 letters");
52
+ }
53
+ return input?.toUpperCase(); // optional: normalize to uppercase
54
+ },
55
+ }),
56
+ utm: Flags.string({
57
+ description: "utm=campaign.source.medium",
58
+ }),
59
+ email: Flags.string({
60
+ description: "email",
61
+ required: true,
62
+ }),
63
+ target: Flags.string({ description: "[mtt] uid of the target" }),
64
+ subject: Flags.string({ description: "[mtt] subject of the email" }),
65
+ body: Flags.string({ description: "[mtt] body of the email" }),
66
+ };
67
+
68
+ create = async (flags) => {
69
+ const values = {
70
+ action: {
71
+ actionType: flags.action_type,
72
+ customFields: flags.customFields,
73
+ /* "mtt": {
74
+ "body": "body",
75
+ "files": [
76
+ "files"
77
+ ],
78
+ "subject": "subject",
79
+ "targets": [
80
+ "targets"
81
+ ]
82
+ },
83
+ */
84
+ testing: flags.testing,
85
+ },
86
+ actionPageId: flags.id,
87
+ contact: {
88
+ address: {
89
+ country: flags.country,
90
+ locality: flags.locality,
91
+ postcode: flags.postcode,
92
+ region: flags.region,
93
+ street: flags.street,
94
+ // "streetNumber": "streetNumber"
95
+ },
96
+ email: flags.email,
97
+ firstName: flags.firstname,
98
+ lastName: flags.lastname,
99
+ phone: flags.phone,
100
+ },
101
+ privacy: {
102
+ // "leadOptIn": true,
103
+ optIn: flags.optin,
104
+ },
105
+ tracking: {
106
+ ...flags.tracking,
107
+ location: "proca-cli/action/add",
108
+ },
109
+ };
110
+
111
+ if (flags.target) {
112
+ values.action.mtt = {
113
+ targets: [flags.target],
114
+ subject: flags.subject || "Test MTT",
115
+ body: flags.body || "Please ignore, this is a test",
116
+ };
117
+ values.action.actionType = "mail2target";
118
+ }
119
+ console.log(values.action.mtt);
120
+
121
+ const query = gql`
122
+ mutation (
123
+ $action: ActionInput!
124
+ $actionPageId: Int!
125
+ $contact: ContactInput!
126
+ $contactRef: ID
127
+ $privacy: ConsentInput!
128
+ $tracking: TrackingInput
129
+ ) {
130
+ addActionContact(
131
+ action: $action
132
+ actionPageId: $actionPageId
133
+ contact: $contact
134
+ contactRef: $contactRef
135
+ privacy: $privacy
136
+ tracking: $tracking
137
+ ) {
138
+ contactRef
139
+ firstName
140
+ }
141
+ }`;
142
+
143
+ const result = await mutation(query, values);
144
+
145
+ console.log("result", result);
146
+ return result;
147
+ };
148
+
149
+ parseUnknownFlags = (argv) => {
150
+ const knownFlags = Object.entries(ActionAdd.flags).flatMap(([key, def]) => {
151
+ const chars = def.char ? [def.char] : [];
152
+ return [key, ...chars];
153
+ });
154
+ /* doesn't work static=false has no effect const unknownFlags = Object.fromEntries(
155
+ argv
156
+ .filter(arg =>
157
+ (/^--?\w+=/.test(arg)) // --key=val or -x=val
158
+ )
159
+ .map(arg => {
160
+ const keyval = arg.replace(/^-+/, '').split('=')
161
+ return [keyval[0], keyval[1]]
162
+ })
163
+ .filter(([key]) => !knownFlags.includes(key))
164
+ )
165
+ */
166
+
167
+ // Extract key=val style positional args (e.g. foo=bar)
168
+ const kvArgs = Object.fromEntries(
169
+ argv
170
+ .filter((arg) => !arg.startsWith("-") && arg.includes("="))
171
+ .map((arg) => arg.split("=")),
172
+ );
173
+
174
+ if (!Object.keys(kvArgs).length) return undefined;
175
+
176
+ return kvArgs;
177
+ };
178
+ async run() {
179
+ const { args, flags } = await this.parse(ActionAdd, {
180
+ context: { strict: false /* this does not work*/ },
181
+ });
182
+
183
+ const customFields = this.parseUnknownFlags(this.argv);
184
+ if (customFields) flags.customFields = JSON.stringify(customFields);
185
+ if (flags.utm) {
186
+ const [campaign, source, medium] = flags.utm.split(".");
187
+ flags.tracking = { source, medium, campaign };
188
+ } else flags.tracking = {};
189
+ const data = await this.create(flags);
190
+ return this.output(data);
191
+ }
192
+ }
@@ -62,6 +62,7 @@ export default class CampaignGet extends Command {
62
62
  name: name,
63
63
  withStats: this.flags.stats,
64
64
  });
65
+ result.campaign.config = JSON.parse(result.campaign.config);
65
66
  return result.campaign;
66
67
  };
67
68
 
@@ -108,7 +109,6 @@ export default class CampaignGet extends Command {
108
109
  };
109
110
 
110
111
  table = (r) => {
111
- r.config = JSON.parse(r.config);
112
112
  super.table(r, null, null);
113
113
  if (this.flags.locale) {
114
114
  this.prettyJson(r.config?.locales[this.flags.locale]);
@@ -18,11 +18,11 @@ export default class CampaignList extends Command {
18
18
 
19
19
  static description = "list all the campaigns";
20
20
 
21
- // static args = this.multiid();
21
+ static args = this.namearg();
22
22
 
23
23
  static flags = {
24
24
  // flag with no value (-f, --force)
25
- ...this.flagify({ name: "name of the organisation" }),
25
+ ...this.flagify({ name: "name of the organisation", char: "o" }),
26
26
  title: Flags.string({
27
27
  char: "t",
28
28
  description: "name of the campaign",
@@ -111,7 +111,7 @@ export default class CampaignList extends Command {
111
111
 
112
112
  if (!flags.title && !flags.name) {
113
113
  throw new Error(
114
- `${this.id} -t [title of the campaign] or -o [organisation]`,
114
+ `${this.id} -t [title of the campaign] or -n [organisation]`,
115
115
  );
116
116
  }
117
117
 
@@ -1,6 +1,6 @@
1
1
  import { Flags } from "@oclif/core";
2
2
  import Command from "#src/procaCommand.mjs";
3
- import { gql, mutation, query } from "#src/urql.mjs";
3
+ import { gql, mutation } from "#src/urql.mjs";
4
4
 
5
5
  const SERVICE_NAMES = [
6
6
  "MAILJET",
@@ -14,9 +14,19 @@ const SERVICE_NAMES = [
14
14
  "SMTP",
15
15
  ].map((d) => d.toLowerCase());
16
16
 
17
- export default class OrgEmail extends Command {
17
+ export default class ServiceAdd extends Command {
18
18
  static description =
19
- "Set service, usually email backend for an org, the specific meaning of each param is dependant on the service";
19
+ "Set service, usually email backend for an org. the specific meaning of each param is dependant on the service. \nIf a service from that type exists, it will replace it";
20
+
21
+ // examples to add to help
22
+ // <%= config.bin %> resolves to the executable name
23
+ // <%= command.id %> resolves to the command name
24
+ static examples = [
25
+ // Examples can be simple strings
26
+ "<%= config.bin %> <%= command.id %> -o example_org --type system",
27
+ '<%= config.bin %> <%= command.id %> -o example_org --host=tls://mail.example.org:587 --user=login --password "secret" --type smtp',
28
+ '<%= config.bin %> <%= command.id %> -o example_org --host=ssl://mail.example.org:465 --user=login --password "secret" --type smtp',
29
+ ];
20
30
 
21
31
  static flags = {
22
32
  ...super.globalFlags,
@@ -29,7 +39,6 @@ export default class OrgEmail extends Command {
29
39
  description: "type of the service",
30
40
  options: SERVICE_NAMES,
31
41
  required: true,
32
- default: "system",
33
42
  }),
34
43
  user: Flags.string({
35
44
  description: "credential of the account on the service",
@@ -37,7 +46,7 @@ export default class OrgEmail extends Command {
37
46
  password: Flags.string({
38
47
  description: "credential of the account on the service",
39
48
  }),
40
- host: Flags.string({
49
+ host: Flags.url({
41
50
  description: "server of the service",
42
51
  }),
43
52
  path: Flags.string({
@@ -0,0 +1,63 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { Flags } from "@oclif/core";
4
+ import { update } from "#src/commands/widget/external/index.mjs";
5
+ import Command from "#src/procaCommand.mjs";
6
+
7
+ export default class CounterExternal extends Command {
8
+ static description =
9
+ "Pull all external counters and save it into a widget extra Supporter. symlink the widget json into config/counter";
10
+
11
+ static examples = ["<%= config.bin %> <%= command.id %>"];
12
+
13
+ static flags = {
14
+ "dry-run": Flags.boolean({
15
+ description: "just fetch, don't update",
16
+ }),
17
+ };
18
+
19
+ async monitored() {
20
+ const dir = path.join(this.config.procaConfig.folder, "/counter");
21
+ try {
22
+ await fs.access(dir);
23
+ } catch {
24
+ this.error(`create the folder ${dir}`);
25
+ }
26
+ const entries = await fs.readdir(dir);
27
+ const results = await Promise.all(
28
+ entries.map(async (entry) => {
29
+ const file = entry.split(".");
30
+ if (file.length !== 2) {
31
+ this.warn(`invalid file ${entry}`);
32
+ return;
33
+ }
34
+ if (file[1] !== "json") {
35
+ this.warn(`should be a json file ${entry}`);
36
+ return;
37
+ }
38
+ const content = JSON.parse(
39
+ await fs.readFile(path.join(dir, entry), "utf-8"),
40
+ );
41
+ return {
42
+ id: Number.parseInt(file[0]),
43
+ name: content.filename,
44
+ url: content.component.counter?.url,
45
+ path: content.component.counter?.path,
46
+ };
47
+ }),
48
+ );
49
+ return results.flat();
50
+ }
51
+
52
+ async run() {
53
+ const { flags } = await this.parse(CounterExternal);
54
+ const widgets = await this.monitored();
55
+ if (flags["dry-run"]) return this.output(widgets);
56
+ const updated = await Promise.all(
57
+ widgets.map(async (widget) => {
58
+ return await update(widget);
59
+ }),
60
+ );
61
+ return this.output(updated);
62
+ }
63
+ }
@@ -1,8 +1,19 @@
1
- import { readFile } from "node:fs/promises";
2
1
  import { Flags } from "@oclif/core";
3
2
  import oPath from "object-path";
4
3
  import { updateCounter } from "#src/commands/widget/update/external.mjs";
5
- import Command from "#src/procaCommand.mjs";
4
+ import Command from "#src/gitCommand.mjs";
5
+
6
+ export const update = async (config) => {
7
+ const d = new CounterExternal([]);
8
+ // const config = await d.getCounterConfig({id});
9
+ if (!config) {
10
+ console.warn("missing config");
11
+ return undefined;
12
+ }
13
+ const counter = await d.fetchCounter(config);
14
+ await updateCounter(config.id, counter);
15
+ return { name: config.name, counter, id: config.id };
16
+ };
6
17
 
7
18
  export default class CounterExternal extends Command {
8
19
  static description =
@@ -41,47 +52,59 @@ export default class CounterExternal extends Command {
41
52
  }),
42
53
  };
43
54
 
44
- async fetchCounter({ url, path, timeout, "dry-run": verbose }) {
45
- const response = await fetch(url, {
46
- timeout,
47
- headers: {
48
- "User-Agent": "proca",
49
- },
50
- });
51
-
52
- if (!response.ok) {
53
- this.error(`API request failed with status ${response.status}`, {
54
- exit: 1,
55
+ async fetchCounter({ url, path, timeout = 10000, "dry-run": verbose }) {
56
+ try {
57
+ const response = await fetch(url, {
58
+ signal: AbortSignal.timeout(timeout),
59
+ headers: {
60
+ "User-Agent": "proca/451.42",
61
+ },
55
62
  });
56
- }
63
+ if (!response.ok) {
64
+ this.error(`API request failed with status ${response.status}`, {
65
+ exit: 1,
66
+ });
67
+ }
57
68
 
58
- const data = await response.json();
59
- if (verbose) {
60
- this.log(JSON.stringify(data, null, 2));
69
+ const data = await response.json();
70
+ if (verbose) {
71
+ this.log(JSON.stringify(data, null, 2));
72
+ }
73
+ const counter = oPath.get(data, path);
74
+ if (
75
+ Number.isNaN(Number.parseFloat(counter)) ||
76
+ !Number.isFinite(counter)
77
+ ) {
78
+ this.error(`Could not extract value from ${counter} at ${path}`, {
79
+ exit: 1,
80
+ });
81
+ }
82
+ return Number.parseFloat(counter);
83
+ } catch (err) {
84
+ if (err.name === "TimeoutError") {
85
+ console.error("Request timed out after", timeout, "ms");
86
+ } else if (err.cause?.code === "ETIMEDOUT") {
87
+ console.error("Network timeout — server unreachable:", url);
88
+ } else {
89
+ throw err;
90
+ }
61
91
  }
62
- const counter = oPath.get(data, path);
63
- if (Number.isNaN(Number.parseFloat(counter)) || !Number.isFinite(counter)) {
64
- this.error(`Could not extract value from ${counter} at ${path}`, {
65
- exit: 1,
66
- });
67
- }
68
- return Number.parseFloat(counter);
69
92
  }
70
93
 
71
- async getCounterConfig(id) {
72
- const folder = this.procaConfig.folder;
73
- const filePath = `${folder}/${id}.json`;
74
-
75
- const data = await readFile(filePath, "utf8");
76
- const jsonData = JSON.parse(data);
77
- return jsonData.component.counter;
94
+ async getCounterConfig() {
95
+ const data = await this.read();
96
+ if (!data.component.counter)
97
+ this.error(
98
+ "missing config.component.counter {url, path} in ${this.getFile()}",
99
+ );
100
+ return data.component.counter;
78
101
  }
79
102
 
80
103
  async run() {
81
104
  const { flags } = await this.parse(CounterExternal);
82
105
  let counter = undefined;
83
- if (!flags.url) {
84
- const config = await this.getCounterConfig(flags.id);
106
+ if (!flags.url && !flags.total) {
107
+ const config = await this.getCounterConfig();
85
108
  flags.url = config.url;
86
109
  flags.path = config.path;
87
110
  }
@@ -0,0 +1,173 @@
1
+ import fs from "node:fs";
2
+ //import { load as loadConfig } from "proca/src/config.mjs";
3
+ import path from "node:path";
4
+ import chalk from "chalk";
5
+ import simpleGit from "simple-git";
6
+ import ProcaCommand from "#src/procaCommand.mjs";
7
+ export { Args, Flags } from "#src/index.mjs";
8
+
9
+ export class ProcaGitCommand extends ProcaCommand {
10
+ fileName = undefined;
11
+ newFile = undefined;
12
+
13
+ static extension = "json";
14
+
15
+ initGit = () => {
16
+ if (!fs.existsSync(path.join(this.config.procaConfig.folder, "/.git"))) {
17
+ this.warn("config not on git");
18
+ }
19
+ return simpleGit({ baseDir: this.config.procaConfig.folder });
20
+ };
21
+
22
+ commit = async () => this.git.commit(this.gitMessage(), [this.getFile()]);
23
+
24
+ checkFile = (fileName) => {
25
+ if (fileName.toString().includes("..")) {
26
+ this.error("invalid filename ", fileName);
27
+ }
28
+ return fileName;
29
+ };
30
+ // mkdir -p
31
+ mkdirp = () => {
32
+ fs.mkdirSync(this.getFile(), { recursive: true });
33
+ };
34
+
35
+ getFolder = () => {
36
+ const mapping = { widget: "./", campaign: "campaign/", org: "org/" };
37
+ const type = this.id.split(":")[0];
38
+ const folder = mapping[type];
39
+ if (!folder) this.error(`no folder defined for ${type}`);
40
+ return folder;
41
+ };
42
+
43
+ setFile = (name, ext = "json") => {
44
+ if (!name) {
45
+ const folder = this.getFolder();
46
+ if (folder === "./") {
47
+ //we are dealing with widgets
48
+ name = `${folder}${this._flags.id}`;
49
+ } else {
50
+ name = `${folder}${this._flags.name}`;
51
+ }
52
+ }
53
+ this.fileName = path.join(
54
+ this.config.procaConfig.folder,
55
+ `${this.checkFile(name)}.${ext}`,
56
+ );
57
+ return this.fileName;
58
+ };
59
+
60
+ getFile = () => {
61
+ if (!this.fileName) this.error("you need to setFile first");
62
+ return this.fileName;
63
+ };
64
+
65
+ gitMessage = (_obj) => `proca cli ${this.id}`;
66
+
67
+ fileExists = () => {
68
+ return fs.existsSync(this.getFile());
69
+ };
70
+
71
+ uncommited = async ({ file, commit = false, exit = true }) => {
72
+ if (!this.git) return false;
73
+ if (!file) file = this.getFile();
74
+ const status = await this.git.status();
75
+ const relative = path.relative(this.config.procaConfig.folder, file);
76
+
77
+ const hasChanges = status.modified.includes(relative);
78
+ if (!hasChanges) return false;
79
+ if (commit) {
80
+ this.git.commit(this.gitMessage(data, file));
81
+ }
82
+ if (exit) {
83
+ this.log(await this.diff(file));
84
+ this.error(`Your local changes to ${file} would be overwritten`, {
85
+ code: "git error",
86
+ exit: 1,
87
+ suggestions: ["run with --auto to commit the changes before pulling"],
88
+ });
89
+ }
90
+ return true;
91
+ };
92
+
93
+ read = async ({ file = this.getFile(), commit } = {}) => {
94
+ try {
95
+ let data;
96
+ if (!file) file = getFile();
97
+ const ext = this.constructor.extension;
98
+ console.log("read file", file, ext);
99
+ if (ext === "json") {
100
+ data = JSON.parse(fs.readFileSync(file, "utf8"));
101
+ } else {
102
+ data = fs.readFileSync(file, "utf8");
103
+ }
104
+ if (commit && this.git) {
105
+ const status = await this.git.status();
106
+ const hasChanges = status.modified.includes(file);
107
+ if (hasChanges) {
108
+ this.git.commit(this.gitMessage(data, file));
109
+ }
110
+ }
111
+ return data;
112
+ } catch (e) {
113
+ console.error(`no local copy of ${file}: ${e.message}`);
114
+ return null;
115
+ }
116
+ };
117
+
118
+ stringify = (obj) =>
119
+ `${JSON.stringify(
120
+ obj,
121
+ (_key, value) => (value === null ? undefined : value),
122
+ 2,
123
+ )}\n`;
124
+
125
+ diff = async (file) => {
126
+ const diff = await this.git.diff([file || this.getFile()]);
127
+ const colorDiff = diff
128
+ .split("\n")
129
+ .map((line) => {
130
+ if (line.startsWith("+")) return chalk.green(line);
131
+ if (line.startsWith("-")) return chalk.red(line);
132
+ if (line.startsWith("@@")) return chalk.cyan(line);
133
+ return line;
134
+ })
135
+ .join("\n");
136
+ return colorDiff;
137
+ };
138
+
139
+ save = async (json) => {
140
+ let needToAdd = false;
141
+ const file = this.getFile();
142
+ if (this.git && !this.fileExists(file)) {
143
+ needToAdd = true;
144
+ }
145
+ this.uncommited({ file }); //exit if unsaved, better to always stash?
146
+ fs.writeFileSync(file, this.stringify(json));
147
+ if (this.git) {
148
+ if (needToAdd) {
149
+ await this.git.add(file);
150
+ }
151
+ const diff = await this.diff(file);
152
+ const r = await this.git.commit(this.gitMessage(json), file);
153
+ return { ...r, ...r.summary, diff };
154
+ }
155
+ return { file, git: false };
156
+ };
157
+
158
+ parse = async () => {
159
+ const r = await super.parse();
160
+ if (this._flags.git === false) {
161
+ this.git = undefined;
162
+ }
163
+ this._flags && this.setFile(null, this.constructor.extension);
164
+ return r;
165
+ };
166
+
167
+ async init() {
168
+ await super.init();
169
+ this.git = this.initGit();
170
+ }
171
+ }
172
+
173
+ export default ProcaGitCommand;
@@ -0,0 +1 @@
1
+ export { run, Args, Flags } from "@oclif/core";
@@ -10,7 +10,8 @@ class ProcaCommand extends Command {
10
10
  static enableJsonFlag = true;
11
11
  procaConfig = { url: "https://api.proca.app/api" };
12
12
  format = "human"; // the default formatting
13
- flags = {};
13
+ flags = {}; // the definition
14
+ _flags = undefined; // the cli flags
14
15
 
15
16
  static baseFlags = {
16
17
  env: Flags.string({
@@ -50,6 +51,17 @@ class ProcaCommand extends Command {
50
51
  return args;
51
52
  }
52
53
 
54
+ static namearg() {
55
+ const args = {
56
+ name: Args.string({
57
+ ignoreStdin: true,
58
+ hidden: true,
59
+ description: "convenience, but try to use -n <name> instead",
60
+ }),
61
+ };
62
+ return args;
63
+ }
64
+
53
65
  static flagify({ multiid = false, name = false, char } = {}) {
54
66
  const flags = Object.assign({}, ProcaCommand.baseFlags);
55
67
  if (name || multiid) {
@@ -86,9 +98,14 @@ class ProcaCommand extends Command {
86
98
 
87
99
  async parse() {
88
100
  const parsed = await super.parse();
101
+
102
+ if (parsed.args.name)
103
+ parsed.flags.name = ProcaCommand.safeName(parsed.args.name);
104
+
89
105
  if (this.ctor.args.id_name_dxid === undefined) {
90
106
  return parsed;
91
107
  }
108
+
92
109
  const maybe = parsed.args.id_name_dxid;
93
110
  if (maybe) {
94
111
  const identified = [
@@ -106,9 +123,11 @@ class ProcaCommand extends Command {
106
123
  if (d) parsed.flags.id = d;
107
124
  else parsed.flags.name = ProcaCommand.safeName(maybe);
108
125
  }
126
+
109
127
  if (parsed.flags.dxid) {
110
128
  parsed.flags.id = dxid(parsed.flags.dxid);
111
129
  }
130
+
112
131
  const identified = [
113
132
  parsed.flags.name,
114
133
  parsed.flags.id,
@@ -120,7 +139,7 @@ class ProcaCommand extends Command {
120
139
  code: 1,
121
140
  });
122
141
  }
123
-
142
+ this._flags = parsed.flags;
124
143
  return parsed;
125
144
  }
126
145
 
@@ -132,16 +151,16 @@ class ProcaCommand extends Command {
132
151
  };
133
152
  async init() {
134
153
  await super.init();
154
+ this.debug = debug("proca");
155
+ initHook({ config: this.config });
156
+ this.procaConfig = this.config.procaConfig; // set up from the hooks/init
157
+ // await this.config.runHook('init', { config: this.config });
135
158
  const { flags } = await this.parse();
136
159
  this.flags = flags;
137
160
  if (flags.json) this.format = "json";
138
161
  if (flags.csv) this.format = "csv";
139
162
  if (flags.markdown) this.format = "markdown";
140
163
 
141
- this.debug = debug("proca");
142
- initHook({ config: this.config });
143
- this.procaConfig = this.config.procaConfig; // set up from the hooks/init
144
- // await this.config.runHook('init', { config: this.config });
145
164
  createClient(this.procaConfig);
146
165
  }
147
166
 
@@ -192,7 +211,7 @@ class ProcaCommand extends Command {
192
211
  for (const [key, value] of Object.entries(d)) {
193
212
  if (key === "__typename") continue;
194
213
  if (key === "config" && typeof value === "string") continue; // it's just a giant mess if not processed, let's skipt
195
- if (value === null) continue;
214
+ if (value === null || value === "") continue;
196
215
  if (typeof value === "string" || typeof value === "number") {
197
216
  r[key] = value;
198
217
  continue;