@skills-store/rednote 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.
@@ -0,0 +1,280 @@
1
+ #!/usr/bin/env -S node --experimental-strip-types
2
+
3
+ import { parseArgs } from 'node:util';
4
+ import {
5
+ findSpec,
6
+ inspectBrowserInstance,
7
+ isDefaultInstanceName,
8
+ isLastConnectMatch,
9
+ readInstanceStore,
10
+ type BrowserInstanceInfo,
11
+ type BrowserType,
12
+ type PersistedInstance,
13
+ } from '../utils/browser-core.ts';
14
+ import { debugLog, printJson, runCli, stringifyError } from '../utils/browser-cli.ts';
15
+
16
+ export type RednoteLoginStatus = 'logged-in' | 'logged-out' | 'unknown';
17
+
18
+ export type RednoteAccountStatus = {
19
+ loginStatus: RednoteLoginStatus;
20
+ lastLoginAt: string | null;
21
+ };
22
+
23
+ export type RednoteStatusSource = 'argument' | 'last-connect' | 'single-instance';
24
+
25
+ export type RednoteStatusTarget = {
26
+ scope: 'default' | 'custom';
27
+ instanceName: string;
28
+ browser: BrowserType;
29
+ userDataDir: string | null;
30
+ createdAt: string | null;
31
+ lastConnect: boolean;
32
+ source: RednoteStatusSource;
33
+ };
34
+
35
+ export type ResolveRednoteAccountStatusContext = RednoteStatusTarget;
36
+
37
+ export type RednoteAccountStatusProvider = (
38
+ context: ResolveRednoteAccountStatusContext,
39
+ ) => Promise<RednoteAccountStatus> | RednoteAccountStatus;
40
+
41
+ export type RednoteInstanceState = 'running' | 'stopped' | 'missing' | 'stale-lock';
42
+
43
+ export type RednoteStatusResult = {
44
+ ok: true;
45
+ instance: {
46
+ scope: 'default' | 'custom';
47
+ name: string;
48
+ browser: BrowserType;
49
+ source: RednoteStatusSource;
50
+ status: RednoteInstanceState;
51
+ exists: boolean;
52
+ inUse: boolean;
53
+ pid: number | null;
54
+ remotePort: number | null;
55
+ userDataDir: string;
56
+ createdAt: string | null;
57
+ lastConnect: boolean;
58
+ };
59
+ rednote: RednoteAccountStatus;
60
+ };
61
+
62
+ export type StatusCliValues = {
63
+ instance?: string;
64
+ help?: boolean;
65
+ };
66
+
67
+ let rednoteAccountStatusProvider: RednoteAccountStatusProvider = async () => ({
68
+ loginStatus: 'unknown',
69
+ lastLoginAt: null,
70
+ });
71
+
72
+ export function registerRednoteAccountStatusProvider(provider: RednoteAccountStatusProvider) {
73
+ rednoteAccountStatusProvider = provider;
74
+ }
75
+
76
+ export async function getRednoteAccountStatus(context: ResolveRednoteAccountStatusContext) {
77
+ return await rednoteAccountStatusProvider(context);
78
+ }
79
+
80
+ function printStatusHelp() {
81
+ process.stdout.write(`rednote status
82
+
83
+ Usage:
84
+ npx -y @skills-store/rednote status [--instance NAME]
85
+ node --experimental-strip-types ./scripts/rednote/status.ts [--instance NAME]
86
+ bun ./scripts/rednote/status.ts [--instance NAME]
87
+
88
+ Options:
89
+ --instance NAME Show status for a custom instance or default browser instance
90
+ -h, --help Show this help
91
+ `);
92
+ }
93
+
94
+ function toInstanceState(instance: BrowserInstanceInfo): RednoteInstanceState {
95
+ if (instance.staleLock) {
96
+ return 'stale-lock';
97
+ }
98
+
99
+ if (instance.inUse) {
100
+ return 'running';
101
+ }
102
+
103
+ if (instance.exists) {
104
+ return 'stopped';
105
+ }
106
+
107
+ return 'missing';
108
+ }
109
+
110
+ function fromPersistedInstance(
111
+ instance: PersistedInstance,
112
+ source: RednoteStatusSource,
113
+ ): RednoteStatusTarget {
114
+ const store = readInstanceStore();
115
+
116
+ return {
117
+ scope: 'custom',
118
+ instanceName: instance.name,
119
+ browser: instance.browser,
120
+ userDataDir: instance.userDataDir,
121
+ createdAt: instance.createdAt,
122
+ lastConnect: isLastConnectMatch(store.lastConnect, 'custom', instance.name, instance.browser),
123
+ source,
124
+ };
125
+ }
126
+
127
+ export function resolveStatusTarget(instanceName?: string): RednoteStatusTarget {
128
+ const store = readInstanceStore();
129
+
130
+ if (instanceName) {
131
+ const normalizedName = instanceName.trim();
132
+ if (!normalizedName) {
133
+ throw new Error('Instance name cannot be empty');
134
+ }
135
+
136
+ if (isDefaultInstanceName(normalizedName)) {
137
+ const browser = normalizedName as BrowserType;
138
+ return {
139
+ scope: 'default',
140
+ instanceName: browser,
141
+ browser,
142
+ userDataDir: null,
143
+ createdAt: null,
144
+ lastConnect: isLastConnectMatch(store.lastConnect, 'default', browser, browser),
145
+ source: 'argument',
146
+ };
147
+ }
148
+
149
+ const persisted = store.instances.find((item) => item.name === normalizedName);
150
+ if (!persisted) {
151
+ throw new Error(`Unknown instance: ${normalizedName}`);
152
+ }
153
+
154
+ return {
155
+ scope: 'custom',
156
+ instanceName: persisted.name,
157
+ browser: persisted.browser,
158
+ userDataDir: persisted.userDataDir,
159
+ createdAt: persisted.createdAt,
160
+ lastConnect: isLastConnectMatch(store.lastConnect, 'custom', persisted.name, persisted.browser),
161
+ source: 'argument',
162
+ };
163
+ }
164
+
165
+ if (store.lastConnect) {
166
+ if (store.lastConnect.scope === 'default') {
167
+ return {
168
+ scope: 'default',
169
+ instanceName: store.lastConnect.name,
170
+ browser: store.lastConnect.browser,
171
+ userDataDir: null,
172
+ createdAt: null,
173
+ lastConnect: true,
174
+ source: 'last-connect',
175
+ };
176
+ }
177
+
178
+ const persisted = store.instances.find((item) => item.name === store.lastConnect?.name);
179
+ if (persisted) {
180
+ return {
181
+ scope: 'custom',
182
+ instanceName: persisted.name,
183
+ browser: persisted.browser,
184
+ userDataDir: persisted.userDataDir,
185
+ createdAt: persisted.createdAt,
186
+ lastConnect: true,
187
+ source: 'last-connect',
188
+ };
189
+ }
190
+ }
191
+
192
+ if (store.instances.length === 1) {
193
+ return fromPersistedInstance(store.instances[0], 'single-instance');
194
+ }
195
+
196
+ throw new Error('No current instance found. Use --instance NAME or connect an instance first.');
197
+ }
198
+
199
+ export async function getRednoteStatus(target: RednoteStatusTarget): Promise<RednoteStatusResult> {
200
+ debugLog('status', 'get status start', { target });
201
+ const spec = findSpec(target.browser);
202
+ const inspected = await inspectBrowserInstance(
203
+ spec,
204
+ undefined,
205
+ target.scope === 'custom' ? target.userDataDir ?? undefined : undefined,
206
+ );
207
+ debugLog('status', 'instance inspected', { inspected });
208
+
209
+ let rednote = await getRednoteAccountStatus(target);
210
+ debugLog('status', 'account status provider result', { rednote });
211
+ if (rednote.loginStatus === 'unknown') {
212
+ try {
213
+ debugLog('status', 'login status unknown, fallback to checkRednoteLogin', { target });
214
+ const { checkRednoteLogin } = await import('./checkLogin.ts');
215
+ const checked = await checkRednoteLogin(target);
216
+ debugLog('status', 'fallback checkRednoteLogin succeeded', { checked });
217
+ rednote = {
218
+ loginStatus: checked.loginStatus,
219
+ lastLoginAt: checked.lastLoginAt,
220
+ };
221
+ } catch (error) {
222
+ debugLog('status', 'fallback checkRednoteLogin failed', { error: stringifyError(error) });
223
+ rednote = {
224
+ loginStatus: 'unknown',
225
+ lastLoginAt: null,
226
+ };
227
+ }
228
+ }
229
+
230
+ return {
231
+ ok: true,
232
+ instance: {
233
+ scope: target.scope,
234
+ name: target.instanceName,
235
+ browser: target.browser,
236
+ source: target.source,
237
+ status: toInstanceState(inspected),
238
+ exists: inspected.exists,
239
+ inUse: inspected.inUse,
240
+ pid: inspected.pid,
241
+ remotePort: inspected.remotePort,
242
+ userDataDir: inspected.userDataDir,
243
+ createdAt: target.createdAt,
244
+ lastConnect: target.lastConnect,
245
+ },
246
+ rednote,
247
+ };
248
+ }
249
+
250
+ export async function runStatusCommand(values: StatusCliValues = {}) {
251
+ if (values.help) {
252
+ printStatusHelp();
253
+ return;
254
+ }
255
+
256
+ const target = resolveStatusTarget(values.instance);
257
+ const result = await getRednoteStatus(target);
258
+ printJson(result);
259
+ }
260
+
261
+ async function main() {
262
+ const { values } = parseArgs({
263
+ args: process.argv.slice(2),
264
+ allowPositionals: true,
265
+ strict: false,
266
+ options: {
267
+ instance: { type: 'string' },
268
+ help: { type: 'boolean', short: 'h' },
269
+ },
270
+ });
271
+
272
+ if (values.help) {
273
+ printStatusHelp();
274
+ return;
275
+ }
276
+
277
+ await runStatusCommand(values);
278
+ }
279
+
280
+ runCli(import.meta.url, main);
@@ -0,0 +1,176 @@
1
+ import path from 'node:path';
2
+ import { parseArgs } from 'node:util';
3
+ import { pathToFileURL } from 'node:url';
4
+
5
+ export type BrowserCliValues = {
6
+ browser?: 'chrome' | 'edge' | 'chromium' | 'brave';
7
+ instance?: string;
8
+ name?: string;
9
+ 'executable-path'?: string;
10
+ 'user-data-dir'?: string;
11
+ force?: boolean;
12
+ port?: string;
13
+ timeout?: string;
14
+ 'kill-timeout'?: string;
15
+ 'startup-url'?: string;
16
+ help?: boolean;
17
+ };
18
+
19
+ export type BrowserCliArgs = {
20
+ values: BrowserCliValues;
21
+ positionals: string[];
22
+ };
23
+
24
+ function printHelp(helpText: string) {
25
+ process.stdout.write(helpText);
26
+ }
27
+
28
+ export function parseBrowserCliArgs(argv: string[]): BrowserCliArgs {
29
+ const { values, positionals } = parseArgs({
30
+ args: argv,
31
+ allowPositionals: true,
32
+ strict: false,
33
+ options: {
34
+ browser: { type: 'string' },
35
+ instance: { type: 'string' },
36
+ name: { type: 'string' },
37
+ 'executable-path': { type: 'string' },
38
+ 'user-data-dir': { type: 'string' },
39
+ force: { type: 'boolean' },
40
+ port: { type: 'string' },
41
+ timeout: { type: 'string' },
42
+ 'kill-timeout': { type: 'string' },
43
+ 'startup-url': { type: 'string' },
44
+ help: { type: 'boolean', short: 'h' },
45
+ },
46
+ });
47
+
48
+ return {
49
+ values: values as BrowserCliValues,
50
+ positionals,
51
+ };
52
+ }
53
+
54
+ export function printJson(value: unknown) {
55
+ process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
56
+ }
57
+
58
+ export function printInitBrowserHelp() {
59
+ printHelp(`rednote browser
60
+
61
+ Commands:
62
+ list
63
+ create --name NAME [--browser chrome|edge|chromium|brave] [--port 9222]
64
+ remove --name NAME [--force]
65
+ connect [--instance NAME] [--browser chrome|edge|chromium|brave] [--user-data-dir PATH] [--force] [--port 9222]
66
+
67
+ Examples:
68
+ npx -y @skills-store/rednote browser list
69
+ npx -y @skills-store/rednote browser create --name seller-main --browser chrome --port 9222
70
+ npx -y @skills-store/rednote browser remove --name seller-main
71
+ npx -y @skills-store/rednote browser connect --instance seller-main
72
+ npx -y @skills-store/rednote browser connect --browser edge --user-data-dir /tmp/edge-profile --port 9223
73
+ `);
74
+ }
75
+
76
+ export function printCreateBrowserHelp() {
77
+ printHelp(`rednote browser create
78
+
79
+ Usage:
80
+ npx -y @skills-store/rednote browser create --name NAME [--browser chrome|edge|chromium|brave] [--port 9222]
81
+ bun ./scripts/browser/create-browser.ts --name NAME [--browser chrome|edge|chromium|brave] [--port 9222]
82
+ `);
83
+ }
84
+
85
+ export function printListBrowserHelp() {
86
+ printHelp(`rednote browser list
87
+
88
+ Usage:
89
+ npx -y @skills-store/rednote browser list
90
+ bun ./scripts/browser/list-browser.ts
91
+ `);
92
+ }
93
+
94
+ export function printRemoveBrowserHelp() {
95
+ printHelp(`rednote browser remove
96
+
97
+ Usage:
98
+ npx -y @skills-store/rednote browser remove --name NAME [--force]
99
+ bun ./scripts/browser/remove-browser.ts --name NAME [--force]
100
+ `);
101
+ }
102
+
103
+ export function printConnectBrowserHelp() {
104
+ printHelp(`rednote browser connect
105
+
106
+ Usage:
107
+ npx -y @skills-store/rednote browser connect [--instance NAME] [--browser chrome|edge|chromium|brave] [--user-data-dir PATH] [--force] [--port 9222]
108
+ bun ./scripts/browser/connect-browser.ts [--instance NAME] [--browser chrome|edge|chromium|brave] [--user-data-dir PATH] [--force] [--port 9222]
109
+
110
+ Notes:
111
+ When using --instance without --port, the stored instance port from data.json is used.
112
+ If no stored port exists yet, a random free port is assigned and saved for next time.
113
+ `);
114
+ }
115
+
116
+ export function stringifyError(error: unknown) {
117
+ if (error instanceof Error) {
118
+ return error.message;
119
+ }
120
+ return String(error);
121
+ }
122
+
123
+ export function isDebugEnabled() {
124
+ const value = process.env.REDNOTE_DEBUG?.trim().toLowerCase();
125
+ return value === '1' || value === 'true' || value === 'yes' || value === 'on';
126
+ }
127
+
128
+ export function debugLog(scope: string, message: string, payload?: Record<string, unknown>) {
129
+ if (!isDebugEnabled()) {
130
+ return;
131
+ }
132
+
133
+ const time = new Date().toISOString();
134
+ const suffix = payload ? ` ${JSON.stringify(payload)}` : '';
135
+ process.stderr.write(`[rednote-debug][${time}][${scope}] ${message}${suffix}
136
+ `);
137
+ }
138
+
139
+ export function isMainModule(metaUrl: string) {
140
+ const entryArg = process.argv[1];
141
+ if (!entryArg) {
142
+ return false;
143
+ }
144
+ return metaUrl === pathToFileURL(path.resolve(entryArg)).href;
145
+ }
146
+
147
+ async function finalizeCliProcess(exitCode: number) {
148
+ await new Promise<void>((resolve) => process.stdout.write('', () => resolve()));
149
+ await new Promise<void>((resolve) => process.stderr.write('', () => resolve()));
150
+ process.exit(exitCode);
151
+ }
152
+
153
+ export function runCli(metaUrl: string, main: () => Promise<void>) {
154
+ if (!isMainModule(metaUrl)) {
155
+ return;
156
+ }
157
+
158
+ main()
159
+ .then(async () => {
160
+ await finalizeCliProcess(0);
161
+ })
162
+ .catch(async (error) => {
163
+ process.stderr.write(
164
+ `${JSON.stringify(
165
+ {
166
+ ok: false,
167
+ error: stringifyError(error),
168
+ },
169
+ null,
170
+ 2,
171
+ )}
172
+ `,
173
+ );
174
+ await finalizeCliProcess(1);
175
+ });
176
+ }