@staqon.one/cli 0.0.0-stage → 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/README.md +155 -2
- package/dist/api/client.js +71 -0
- package/dist/auth.js +102 -0
- package/dist/commands/ad.js +82 -0
- package/dist/commands/auth.js +37 -0
- package/dist/commands/members.js +111 -0
- package/dist/commands/messaging.js +92 -0
- package/dist/commands/misc.js +94 -0
- package/dist/commands/verify.js +64 -0
- package/dist/config.js +37 -0
- package/dist/index.js +24 -0
- package/dist/output.js +24 -0
- package/package.json +30 -4
- package/src/api/client.ts +84 -0
- package/src/auth.ts +132 -0
- package/src/commands/ad.ts +100 -0
- package/src/commands/auth.ts +39 -0
- package/src/commands/members.ts +130 -0
- package/src/commands/messaging.ts +116 -0
- package/src/commands/misc.ts +109 -0
- package/src/commands/verify.ts +75 -0
- package/src/config.ts +48 -0
- package/src/index.ts +30 -0
- package/src/output.ts +24 -0
- package/tsconfig.json +15 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { proxy, MAX_LIST_LIMIT } from '../api/client.js';
|
|
3
|
+
import { fail, printJson, ok } from '../output.js';
|
|
4
|
+
|
|
5
|
+
function teamOpt(cmd: Command): Command {
|
|
6
|
+
return cmd.option('--team <idOrSlug>', 'team id or slug — only needed if the workspace has more than one eligible team');
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function registerMemberCommands(program: Command): void {
|
|
10
|
+
const members = program.command('members').description('Manage external team members');
|
|
11
|
+
|
|
12
|
+
teamOpt(
|
|
13
|
+
members
|
|
14
|
+
.command('list')
|
|
15
|
+
.description('List this team\'s external (guest) members')
|
|
16
|
+
.option('--page <n>', 'page number', '1')
|
|
17
|
+
.option('--limit <n>', `page size, 1-${MAX_LIST_LIMIT} (default 50)`, '50')
|
|
18
|
+
.option('--search <text>', 'match against name or email'),
|
|
19
|
+
).action(async (opts: { page: string; limit: string; search?: string; team?: string }) => {
|
|
20
|
+
try {
|
|
21
|
+
const body = await proxy('external/members', {
|
|
22
|
+
method: 'GET',
|
|
23
|
+
team: opts.team,
|
|
24
|
+
query: { page: opts.page, limit: opts.limit, search: opts.search },
|
|
25
|
+
});
|
|
26
|
+
printJson(body);
|
|
27
|
+
} catch (e) {
|
|
28
|
+
fail(e);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
teamOpt(
|
|
33
|
+
members
|
|
34
|
+
.command('get')
|
|
35
|
+
.description('Get one external member, by email or member_id')
|
|
36
|
+
.option('--email <email>')
|
|
37
|
+
.option('--member-id <id>'),
|
|
38
|
+
).action(async (opts: { email?: string; memberId?: string; team?: string }) => {
|
|
39
|
+
try {
|
|
40
|
+
const body = await proxy('external/member', {
|
|
41
|
+
method: 'GET',
|
|
42
|
+
team: opts.team,
|
|
43
|
+
query: { email: opts.email, member_id: opts.memberId },
|
|
44
|
+
});
|
|
45
|
+
printJson(body);
|
|
46
|
+
} catch (e) {
|
|
47
|
+
fail(e);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
teamOpt(
|
|
52
|
+
members
|
|
53
|
+
.command('add')
|
|
54
|
+
.description('Add (or re-add) a member to this external team')
|
|
55
|
+
.requiredOption('--name <name>')
|
|
56
|
+
.requiredOption('--email <email>'),
|
|
57
|
+
// NOTE: no --phone flag. add-member.php's request schema is {name, email} only (both
|
|
58
|
+
// required) — verified against openapi.yaml this session. The old CLI sent a --phone
|
|
59
|
+
// value the server silently ignored; that flag is dropped here rather than kept as a
|
|
60
|
+
// no-op, so nobody mistakes it for working.
|
|
61
|
+
).action(async (opts: { name: string; email: string; team?: string }) => {
|
|
62
|
+
try {
|
|
63
|
+
const body = await proxy('external/add-member', {
|
|
64
|
+
method: 'POST',
|
|
65
|
+
team: opts.team,
|
|
66
|
+
json: { name: opts.name, email: opts.email },
|
|
67
|
+
});
|
|
68
|
+
printJson(body);
|
|
69
|
+
} catch (e) {
|
|
70
|
+
fail(e);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
teamOpt(
|
|
75
|
+
members
|
|
76
|
+
.command('remove')
|
|
77
|
+
.description('Remove a member from this external team (archives their thread; history kept)')
|
|
78
|
+
.requiredOption('--email <email>'),
|
|
79
|
+
).action(async (opts: { email: string; team?: string }) => {
|
|
80
|
+
try {
|
|
81
|
+
const body = await proxy('external/remove-member', {
|
|
82
|
+
method: 'POST',
|
|
83
|
+
team: opts.team,
|
|
84
|
+
json: { email: opts.email },
|
|
85
|
+
});
|
|
86
|
+
printJson(body);
|
|
87
|
+
} catch (e) {
|
|
88
|
+
fail(e);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
teamOpt(
|
|
93
|
+
members
|
|
94
|
+
.command('teams')
|
|
95
|
+
.description('Which external teams has a linked member joined? (looked up by external_ref)')
|
|
96
|
+
.requiredOption('--external-ref <ref>', 'the external_ref originally sent to link-invite'),
|
|
97
|
+
).action(async (opts: { externalRef: string; team?: string }) => {
|
|
98
|
+
try {
|
|
99
|
+
const body = await proxy('external/member-teams', {
|
|
100
|
+
method: 'GET',
|
|
101
|
+
team: opts.team,
|
|
102
|
+
query: { external_ref: opts.externalRef },
|
|
103
|
+
});
|
|
104
|
+
printJson(body);
|
|
105
|
+
} catch (e) {
|
|
106
|
+
fail(e);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// Internal-surface member listing — same family, different endpoint/scope (no guest filter, reports role not message stats).
|
|
111
|
+
teamOpt(
|
|
112
|
+
program
|
|
113
|
+
.command('internal-members')
|
|
114
|
+
.description('List this key\'s internal team members (role-based, no external thread stats)')
|
|
115
|
+
.option('--page <n>', 'page number', '1')
|
|
116
|
+
.option('--limit <n>', `page size, 1-${MAX_LIST_LIMIT} (default 50)`, '50')
|
|
117
|
+
.option('--search <text>', 'match against name, email or username'),
|
|
118
|
+
).action(async (opts: { page: string; limit: string; search?: string; team?: string }) => {
|
|
119
|
+
try {
|
|
120
|
+
const body = await proxy('internal/members', {
|
|
121
|
+
method: 'GET',
|
|
122
|
+
team: opts.team,
|
|
123
|
+
query: { page: opts.page, limit: opts.limit, search: opts.search },
|
|
124
|
+
});
|
|
125
|
+
printJson(body);
|
|
126
|
+
} catch (e) {
|
|
127
|
+
fail(e);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { proxy, MAX_LIST_LIMIT } from '../api/client.js';
|
|
3
|
+
import { fail, printJson } from '../output.js';
|
|
4
|
+
|
|
5
|
+
function teamOpt(cmd: Command): Command {
|
|
6
|
+
return cmd.option('--team <idOrSlug>', 'team id or slug — only needed if the workspace has more than one eligible team');
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function registerMessagingCommands(program: Command): void {
|
|
10
|
+
const messages = program.command('messages').description('Send and read External team member conversations');
|
|
11
|
+
|
|
12
|
+
teamOpt(
|
|
13
|
+
messages
|
|
14
|
+
.command('send')
|
|
15
|
+
.description('Send a message into a member\'s thread, as the workspace owner')
|
|
16
|
+
.requiredOption('--email <email>')
|
|
17
|
+
.requiredOption('--message <text>', 'max 60000 characters'),
|
|
18
|
+
).action(async (opts: { email: string; message: string; team?: string }) => {
|
|
19
|
+
try {
|
|
20
|
+
const body = await proxy('external/message', {
|
|
21
|
+
method: 'POST',
|
|
22
|
+
team: opts.team,
|
|
23
|
+
json: { email: opts.email, message: opts.message },
|
|
24
|
+
});
|
|
25
|
+
printJson(body);
|
|
26
|
+
} catch (e) {
|
|
27
|
+
fail(e);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
teamOpt(
|
|
32
|
+
messages
|
|
33
|
+
.command('comment')
|
|
34
|
+
.description(
|
|
35
|
+
'Post a comment into a member\'s thread, attributed to the workspace (no consent link needed — writing in is the workspace\'s own authority)',
|
|
36
|
+
)
|
|
37
|
+
.requiredOption('--username <username>')
|
|
38
|
+
.requiredOption('--comment <text>', 'max 60000 characters')
|
|
39
|
+
.option(
|
|
40
|
+
'--external-ref <ref>',
|
|
41
|
+
'idempotency key, max 190 chars — scoped to the WHOLE WORKSPACE, not per recipient; reusing it for a different recipient silently does not reach them',
|
|
42
|
+
),
|
|
43
|
+
).action(async (opts: { username: string; comment: string; externalRef?: string; team?: string }) => {
|
|
44
|
+
try {
|
|
45
|
+
const body = await proxy('external/comment', {
|
|
46
|
+
method: 'POST',
|
|
47
|
+
team: opts.team,
|
|
48
|
+
json: { username: opts.username, comment: opts.comment, external_ref: opts.externalRef },
|
|
49
|
+
});
|
|
50
|
+
printJson(body);
|
|
51
|
+
} catch (e) {
|
|
52
|
+
fail(e);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
teamOpt(
|
|
57
|
+
messages
|
|
58
|
+
.command('read')
|
|
59
|
+
.description('Read a member\'s thread history — requires an APPROVED consent link for that member (403 if not present)')
|
|
60
|
+
.requiredOption('--username <username>')
|
|
61
|
+
.option('--limit <n>', `1-${MAX_LIST_LIMIT} (default 50)`, '50')
|
|
62
|
+
.option('--before <id>', 'message id cursor — older messages, newest-first')
|
|
63
|
+
.option('--after <id>', 'message id cursor — newer messages, oldest-first (mutually exclusive with --before)'),
|
|
64
|
+
).action(async (opts: { username: string; limit: string; before?: string; after?: string; team?: string }) => {
|
|
65
|
+
if (opts.before && opts.after) fail(new Error('Use either --before or --after, not both.'));
|
|
66
|
+
try {
|
|
67
|
+
const body = await proxy('external/messages', {
|
|
68
|
+
method: 'GET',
|
|
69
|
+
team: opts.team,
|
|
70
|
+
query: { username: opts.username, limit: opts.limit, before: opts.before, after: opts.after },
|
|
71
|
+
});
|
|
72
|
+
printJson(body);
|
|
73
|
+
} catch (e) {
|
|
74
|
+
fail(e);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// Internal surface: one-way feed push, NOT a conversation (no replies/reactions/attachments).
|
|
79
|
+
// Requires an APPROVED link_type='internal' consent row (stricter than External's write path).
|
|
80
|
+
teamOpt(
|
|
81
|
+
program
|
|
82
|
+
.command('internal-message')
|
|
83
|
+
.description('Push a one-way message into a member\'s API Messages feed (internal surface — requires member consent)')
|
|
84
|
+
.requiredOption('--username <username>')
|
|
85
|
+
.requiredOption('--message <text>', 'max 60000 characters')
|
|
86
|
+
.option('--title <text>', 'max 200 characters')
|
|
87
|
+
.option('--link-url <url>', 'must be http(s)')
|
|
88
|
+
.option('--external-ref <ref>', 'idempotency key, max 190 chars — scoped per API key, not per recipient'),
|
|
89
|
+
).action(
|
|
90
|
+
async (opts: {
|
|
91
|
+
username: string;
|
|
92
|
+
message: string;
|
|
93
|
+
title?: string;
|
|
94
|
+
linkUrl?: string;
|
|
95
|
+
externalRef?: string;
|
|
96
|
+
team?: string;
|
|
97
|
+
}) => {
|
|
98
|
+
try {
|
|
99
|
+
const body = await proxy('internal/message', {
|
|
100
|
+
method: 'POST',
|
|
101
|
+
team: opts.team,
|
|
102
|
+
json: {
|
|
103
|
+
username: opts.username,
|
|
104
|
+
message: opts.message,
|
|
105
|
+
title: opts.title,
|
|
106
|
+
link_url: opts.linkUrl,
|
|
107
|
+
external_ref: opts.externalRef,
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
printJson(body);
|
|
111
|
+
} catch (e) {
|
|
112
|
+
fail(e);
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
);
|
|
116
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { proxy, MAX_LIST_LIMIT } from '../api/client.js';
|
|
3
|
+
import { fail, printJson } from '../output.js';
|
|
4
|
+
|
|
5
|
+
function teamOpt(cmd: Command): Command {
|
|
6
|
+
return cmd.option('--team <idOrSlug>', 'team id or slug — only needed if the workspace has more than one eligible team');
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function registerMiscCommands(program: Command): void {
|
|
10
|
+
// ---- Escalation (External surface) ----
|
|
11
|
+
const escalation = program.command('escalation').description('Raise and read escalation matters against team members');
|
|
12
|
+
|
|
13
|
+
teamOpt(
|
|
14
|
+
escalation
|
|
15
|
+
.command('raise')
|
|
16
|
+
.description('Raise an escalation matter against a team member')
|
|
17
|
+
.requiredOption('--email <email>')
|
|
18
|
+
.requiredOption('--title <text>', 'min 2 characters')
|
|
19
|
+
.requiredOption('--description <text>')
|
|
20
|
+
.requiredOption('--due-date <YYYY-MM-DD>', 'must be strictly after today')
|
|
21
|
+
.option('--priority <level>', 'low|medium|high|critical', 'medium'),
|
|
22
|
+
).action(
|
|
23
|
+
async (opts: { email: string; title: string; description: string; dueDate: string; priority: string; team?: string }) => {
|
|
24
|
+
try {
|
|
25
|
+
const body = await proxy('external/escalation', {
|
|
26
|
+
method: 'POST',
|
|
27
|
+
team: opts.team,
|
|
28
|
+
json: {
|
|
29
|
+
email: opts.email,
|
|
30
|
+
title: opts.title,
|
|
31
|
+
description: opts.description,
|
|
32
|
+
due_date: opts.dueDate,
|
|
33
|
+
priority: opts.priority,
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
printJson(body);
|
|
37
|
+
} catch (e) {
|
|
38
|
+
fail(e);
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
teamOpt(
|
|
44
|
+
escalation
|
|
45
|
+
.command('list')
|
|
46
|
+
.description('List escalation matters raised against this team\'s members (scoped to matters your key can see)')
|
|
47
|
+
.option('--page <n>', 'page number', '1')
|
|
48
|
+
.option('--limit <n>', `1-${MAX_LIST_LIMIT} (default 50)`, '50')
|
|
49
|
+
.option('--status <status>', 'open|in_progress|resolved'),
|
|
50
|
+
).action(async (opts: { page: string; limit: string; status?: string; team?: string }) => {
|
|
51
|
+
try {
|
|
52
|
+
const body = await proxy('external/escalations', {
|
|
53
|
+
method: 'GET',
|
|
54
|
+
team: opts.team,
|
|
55
|
+
query: { page: opts.page, limit: opts.limit, status: opts.status },
|
|
56
|
+
});
|
|
57
|
+
printJson(body);
|
|
58
|
+
} catch (e) {
|
|
59
|
+
fail(e);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
teamOpt(
|
|
64
|
+
escalation
|
|
65
|
+
.command('get')
|
|
66
|
+
.description('Fetch a single escalation matter by id')
|
|
67
|
+
.requiredOption('--matter-id <id>'),
|
|
68
|
+
).action(async (opts: { matterId: string; team?: string }) => {
|
|
69
|
+
try {
|
|
70
|
+
const body = await proxy('external/escalations', { method: 'GET', team: opts.team, query: { matter_id: opts.matterId } });
|
|
71
|
+
printJson(body);
|
|
72
|
+
} catch (e) {
|
|
73
|
+
fail(e);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// ---- Status (key context + usage — External and Internal surfaces) ----
|
|
78
|
+
teamOpt(
|
|
79
|
+
program
|
|
80
|
+
.command('status')
|
|
81
|
+
.description('Key context + usage for this team (External surface) — real-only member counts and rate-limit usage')
|
|
82
|
+
.option('--internal', 'use the Internal surface instead (no guest-role filter)'),
|
|
83
|
+
).action(async (opts: { internal?: boolean; team?: string }) => {
|
|
84
|
+
try {
|
|
85
|
+
const body = await proxy(opts.internal ? 'internal/status' : 'external/status', { method: 'GET', team: opts.team });
|
|
86
|
+
printJson(body);
|
|
87
|
+
} catch (e) {
|
|
88
|
+
fail(e);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// ---- Webhook health (External and Internal surfaces) ----
|
|
93
|
+
teamOpt(
|
|
94
|
+
program
|
|
95
|
+
.command('webhooks')
|
|
96
|
+
.description('Health of this team\'s webhooks (a webhook auto-disables after 10 consecutive non-2xx deliveries)')
|
|
97
|
+
.option('--internal', 'use the Internal surface instead'),
|
|
98
|
+
).action(async (opts: { internal?: boolean; team?: string }) => {
|
|
99
|
+
try {
|
|
100
|
+
const body = await proxy(opts.internal ? 'internal/webhook-status' : 'external/webhook-status', {
|
|
101
|
+
method: 'GET',
|
|
102
|
+
team: opts.team,
|
|
103
|
+
});
|
|
104
|
+
printJson(body);
|
|
105
|
+
} catch (e) {
|
|
106
|
+
fail(e);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { proxy } from '../api/client.js';
|
|
3
|
+
import { fail, printJson } from '../output.js';
|
|
4
|
+
|
|
5
|
+
function teamOpt(cmd: Command): Command {
|
|
6
|
+
return cmd.option('--team <idOrSlug>', 'team id or slug — only needed if the workspace has more than one eligible team');
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function registerVerifyCommands(program: Command): void {
|
|
10
|
+
const verify = program.command('verify').description('e-StaqOn Verify — ask a member to push-approve a sensitive action');
|
|
11
|
+
|
|
12
|
+
teamOpt(
|
|
13
|
+
verify
|
|
14
|
+
.command('start')
|
|
15
|
+
.description('Ask a member to confirm a sensitive action (push approval) — requires an approved, unexpired consent link')
|
|
16
|
+
.requiredOption('--username <username>')
|
|
17
|
+
.requiredOption('--action <text>', 'short label, max 100 chars')
|
|
18
|
+
.requiredOption('--context <text>', 'what the member is approving — shown to them verbatim, max 500 chars')
|
|
19
|
+
.option('--ttl <seconds>', 'how long the request stays open, 30-180 (default 30; out-of-range is clamped, not rejected)'),
|
|
20
|
+
).action(
|
|
21
|
+
async (opts: { username: string; action: string; context: string; ttl?: string; team?: string }) => {
|
|
22
|
+
try {
|
|
23
|
+
const body = await proxy('external/staqon-verify-start', {
|
|
24
|
+
method: 'POST',
|
|
25
|
+
team: opts.team,
|
|
26
|
+
json: {
|
|
27
|
+
username: opts.username,
|
|
28
|
+
action: opts.action,
|
|
29
|
+
context: opts.context,
|
|
30
|
+
ttl_seconds: opts.ttl ? Number(opts.ttl) : undefined,
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
printJson(body);
|
|
34
|
+
} catch (e) {
|
|
35
|
+
fail(e);
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
teamOpt(
|
|
41
|
+
verify
|
|
42
|
+
.command('check')
|
|
43
|
+
.description('Poll the outcome of a verification request — only verified:true means the member approved in time')
|
|
44
|
+
.requiredOption('--id <verification_id>'),
|
|
45
|
+
).action(async (opts: { id: string; team?: string }) => {
|
|
46
|
+
try {
|
|
47
|
+
const body = await proxy('external/staqon-verify-check', {
|
|
48
|
+
method: 'POST',
|
|
49
|
+
team: opts.team,
|
|
50
|
+
json: { verification_id: opts.id },
|
|
51
|
+
});
|
|
52
|
+
printJson(body);
|
|
53
|
+
} catch (e) {
|
|
54
|
+
fail(e);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
teamOpt(
|
|
59
|
+
verify
|
|
60
|
+
.command('cancel')
|
|
61
|
+
.description('Withdraw a pending verification request — a 409 means the member already answered; that answer stands')
|
|
62
|
+
.requiredOption('--id <verification_id>'),
|
|
63
|
+
).action(async (opts: { id: string; team?: string }) => {
|
|
64
|
+
try {
|
|
65
|
+
const body = await proxy('external/staqon-verify-cancel', {
|
|
66
|
+
method: 'POST',
|
|
67
|
+
team: opts.team,
|
|
68
|
+
json: { verification_id: opts.id },
|
|
69
|
+
});
|
|
70
|
+
printJson(body);
|
|
71
|
+
} catch (e) {
|
|
72
|
+
fail(e);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { homedir } from 'node:os';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { mkdir, readFile, writeFile, chmod } from 'node:fs/promises';
|
|
4
|
+
|
|
5
|
+
export const BASE_URL = process.env.STAQON_BASE_URL ?? 'https://staqon.one';
|
|
6
|
+
const CONFIG_DIR = join(homedir(), '.staqon');
|
|
7
|
+
const CREDS_PATH = join(CONFIG_DIR, 'credentials.json');
|
|
8
|
+
|
|
9
|
+
export interface StoredSession {
|
|
10
|
+
token: string; // stq_cli_<64 hex> — the CLI's own session token, never a tenant API key
|
|
11
|
+
workspace: { id: number; slug: string; name: string };
|
|
12
|
+
expires_at: string; // informational only; the server is the real authority on expiry
|
|
13
|
+
paired_at: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async function ensureDir(): Promise<void> {
|
|
17
|
+
await mkdir(CONFIG_DIR, { recursive: true });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function loadSession(): Promise<StoredSession | null> {
|
|
21
|
+
try {
|
|
22
|
+
const raw = await readFile(CREDS_PATH, 'utf8');
|
|
23
|
+
return JSON.parse(raw) as StoredSession;
|
|
24
|
+
} catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function saveSession(session: StoredSession): Promise<void> {
|
|
30
|
+
await ensureDir();
|
|
31
|
+
await writeFile(CREDS_PATH, JSON.stringify(session, null, 2), { mode: 0o600 });
|
|
32
|
+
// writeFile's mode option is only honored on file *creation* on some platforms — enforce it explicitly.
|
|
33
|
+
await chmod(CREDS_PATH, 0o600);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function clearSession(): Promise<void> {
|
|
37
|
+
await ensureDir();
|
|
38
|
+
try {
|
|
39
|
+
await writeFile(CREDS_PATH, '', { mode: 0o600 });
|
|
40
|
+
await import('node:fs/promises').then((fs) => fs.unlink(CREDS_PATH));
|
|
41
|
+
} catch {
|
|
42
|
+
// already gone — fine
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function credentialsPath(): string {
|
|
47
|
+
return CREDS_PATH;
|
|
48
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import { registerAuthCommands } from './commands/auth.js';
|
|
4
|
+
import { registerMemberCommands } from './commands/members.js';
|
|
5
|
+
import { registerMessagingCommands } from './commands/messaging.js';
|
|
6
|
+
import { registerVerifyCommands } from './commands/verify.js';
|
|
7
|
+
import { registerAdCommands } from './commands/ad.js';
|
|
8
|
+
import { registerMiscCommands } from './commands/misc.js';
|
|
9
|
+
|
|
10
|
+
const program = new Command();
|
|
11
|
+
|
|
12
|
+
program
|
|
13
|
+
.name('staqon')
|
|
14
|
+
.description(
|
|
15
|
+
'Command-line tool for a StaqOn workspace owner. Sign in with `staqon login` — a device-pairing ' +
|
|
16
|
+
'flow approved from your StaqOn dashboard. The CLI never holds or sees a raw API key.',
|
|
17
|
+
)
|
|
18
|
+
.version('0.1.0');
|
|
19
|
+
|
|
20
|
+
registerAuthCommands(program);
|
|
21
|
+
registerMemberCommands(program);
|
|
22
|
+
registerMessagingCommands(program);
|
|
23
|
+
registerVerifyCommands(program);
|
|
24
|
+
registerAdCommands(program);
|
|
25
|
+
registerMiscCommands(program);
|
|
26
|
+
|
|
27
|
+
program.parseAsync(process.argv).catch((e) => {
|
|
28
|
+
console.error('Error:', e instanceof Error ? e.message : e);
|
|
29
|
+
process.exit(1);
|
|
30
|
+
});
|
package/src/output.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { StaqOnApiError } from './api/client.js';
|
|
2
|
+
|
|
3
|
+
export function ok(message: string): void {
|
|
4
|
+
console.log(message);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function fail(e: unknown): never {
|
|
8
|
+
if (e instanceof StaqOnApiError) {
|
|
9
|
+
console.error(`Error: ${e.message}`);
|
|
10
|
+
if (Object.keys(e.body).length) {
|
|
11
|
+
const extra = Object.fromEntries(Object.entries(e.body).filter(([k]) => k !== 'error' && k !== 'ok'));
|
|
12
|
+
if (Object.keys(extra).length) console.error(JSON.stringify(extra, null, 2));
|
|
13
|
+
}
|
|
14
|
+
} else if (e instanceof Error) {
|
|
15
|
+
console.error(`Error: ${e.message}`);
|
|
16
|
+
} else {
|
|
17
|
+
console.error('Error:', e);
|
|
18
|
+
}
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function printJson(data: unknown): void {
|
|
23
|
+
console.log(JSON.stringify(data, null, 2));
|
|
24
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"outDir": "dist",
|
|
7
|
+
"rootDir": "src",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"declaration": false,
|
|
12
|
+
"resolveJsonModule": true
|
|
13
|
+
},
|
|
14
|
+
"include": ["src/**/*"]
|
|
15
|
+
}
|