@chipallen2/snazi 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 +281 -0
- package/com.soup-nazi.snazi-serve.plist +46 -0
- package/dist/address.js +61 -0
- package/dist/api.js +101 -0
- package/dist/cache.js +173 -0
- package/dist/channels/imessage.js +47 -0
- package/dist/channels/index.js +39 -0
- package/dist/channels/types.js +16 -0
- package/dist/chatdb.js +202 -0
- package/dist/cli.js +516 -0
- package/dist/client.js +106 -0
- package/dist/config.js +110 -0
- package/dist/daemon.js +89 -0
- package/dist/doctor.js +99 -0
- package/dist/init.js +155 -0
- package/dist/server.js +466 -0
- package/package.json +52 -0
package/dist/config.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.DEFAULT_API_URL = exports.CONFIG_PATH = exports.CONFIG_DIR = void 0;
|
|
37
|
+
exports.readConfigIfPresent = readConfigIfPresent;
|
|
38
|
+
exports.loadConfig = loadConfig;
|
|
39
|
+
exports.saveConfig = saveConfig;
|
|
40
|
+
const fs = __importStar(require("fs"));
|
|
41
|
+
const os = __importStar(require("os"));
|
|
42
|
+
const path = __importStar(require("path"));
|
|
43
|
+
exports.CONFIG_DIR = path.join(os.homedir(), '.snazi');
|
|
44
|
+
exports.CONFIG_PATH = path.join(exports.CONFIG_DIR, 'config.json');
|
|
45
|
+
/** Default deployment used by `snazi init` when none is supplied. */
|
|
46
|
+
exports.DEFAULT_API_URL = 'https://snazi.dev';
|
|
47
|
+
/**
|
|
48
|
+
* Read config.json if it exists, WITHOUT exiting on missing/invalid input.
|
|
49
|
+
* Returns the parsed config (apiUrl trailing slash stripped) or null. Used by
|
|
50
|
+
* `snazi init` and `snazi doctor`, which must run before a valid config exists.
|
|
51
|
+
*/
|
|
52
|
+
function readConfigIfPresent() {
|
|
53
|
+
try {
|
|
54
|
+
if (!fs.existsSync(exports.CONFIG_PATH))
|
|
55
|
+
return null;
|
|
56
|
+
const cfg = JSON.parse(fs.readFileSync(exports.CONFIG_PATH, 'utf8'));
|
|
57
|
+
if (cfg && typeof cfg === 'object') {
|
|
58
|
+
if (typeof cfg.apiUrl === 'string')
|
|
59
|
+
cfg.apiUrl = cfg.apiUrl.replace(/\/+$/, '');
|
|
60
|
+
return cfg;
|
|
61
|
+
}
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/** Load and validate config.json. Exits with a helpful message if missing. */
|
|
69
|
+
function loadConfig() {
|
|
70
|
+
if (!fs.existsSync(exports.CONFIG_PATH)) {
|
|
71
|
+
console.error(JSON.stringify({
|
|
72
|
+
error: `Config not found at ${exports.CONFIG_PATH}. Run 'snazi init' to create it.`,
|
|
73
|
+
}));
|
|
74
|
+
process.exit(2);
|
|
75
|
+
}
|
|
76
|
+
let raw;
|
|
77
|
+
try {
|
|
78
|
+
raw = fs.readFileSync(exports.CONFIG_PATH, 'utf8');
|
|
79
|
+
}
|
|
80
|
+
catch (e) {
|
|
81
|
+
console.error(JSON.stringify({ error: `Cannot read config: ${String(e)}` }));
|
|
82
|
+
process.exit(2);
|
|
83
|
+
}
|
|
84
|
+
let cfg;
|
|
85
|
+
try {
|
|
86
|
+
cfg = JSON.parse(raw);
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
console.error(JSON.stringify({ error: 'config.json is not valid JSON.' }));
|
|
90
|
+
process.exit(2);
|
|
91
|
+
}
|
|
92
|
+
if (!cfg.apiUrl || !cfg.apiKey) {
|
|
93
|
+
console.error(JSON.stringify({ error: 'config.json must include apiUrl and apiKey.' }));
|
|
94
|
+
process.exit(2);
|
|
95
|
+
}
|
|
96
|
+
// Normalize: strip trailing slash from apiUrl.
|
|
97
|
+
cfg.apiUrl = cfg.apiUrl.replace(/\/+$/, '');
|
|
98
|
+
if (!Array.isArray(cfg.channels))
|
|
99
|
+
cfg.channels = ['imessage'];
|
|
100
|
+
return cfg;
|
|
101
|
+
}
|
|
102
|
+
/** Persist config back to disk (preserving 0600 perms). */
|
|
103
|
+
function saveConfig(cfg) {
|
|
104
|
+
if (!fs.existsSync(exports.CONFIG_DIR)) {
|
|
105
|
+
fs.mkdirSync(exports.CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
106
|
+
}
|
|
107
|
+
fs.writeFileSync(exports.CONFIG_PATH, JSON.stringify(cfg, null, 2) + '\n', {
|
|
108
|
+
mode: 0o600,
|
|
109
|
+
});
|
|
110
|
+
}
|
package/dist/daemon.js
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.LABEL = void 0;
|
|
37
|
+
exports.installDaemon = installDaemon;
|
|
38
|
+
/**
|
|
39
|
+
* snazi serve --install-daemon — install the launchd LaunchAgent.
|
|
40
|
+
*
|
|
41
|
+
* Writes ~/Library/LaunchAgents/com.soup-nazi.snazi-serve.plist from the
|
|
42
|
+
* template, substituting the real node binary, CLI path, bind, and port. Then
|
|
43
|
+
* prints the `launchctl` load/unload commands and the Full Disk Access note.
|
|
44
|
+
*
|
|
45
|
+
* We intentionally do NOT auto-`launchctl load` here: loading a LaunchAgent and
|
|
46
|
+
* granting Full Disk Access is a deliberate, user-visible action. We print the
|
|
47
|
+
* exact commands instead so it stays transparent.
|
|
48
|
+
*/
|
|
49
|
+
const fs = __importStar(require("fs"));
|
|
50
|
+
const os = __importStar(require("os"));
|
|
51
|
+
const path = __importStar(require("path"));
|
|
52
|
+
const server_1 = require("./server");
|
|
53
|
+
exports.LABEL = 'com.soup-nazi.snazi-serve';
|
|
54
|
+
// Mirrors server.ts port policy (kept local to avoid coupling).
|
|
55
|
+
function pickPort(cfg, port) {
|
|
56
|
+
const p = port ?? cfg.servePort ?? 8787;
|
|
57
|
+
if (!Number.isInteger(p) || p < 1 || p > 65535) {
|
|
58
|
+
throw new Error(`Invalid port: ${p}`);
|
|
59
|
+
}
|
|
60
|
+
return p;
|
|
61
|
+
}
|
|
62
|
+
function installDaemon(cfg, opts) {
|
|
63
|
+
const node = process.execPath;
|
|
64
|
+
// dist/daemon.js -> dist/cli.js
|
|
65
|
+
const cli = path.join(__dirname, 'cli.js');
|
|
66
|
+
if (!fs.existsSync(cli)) {
|
|
67
|
+
throw new Error(`Cannot find compiled CLI at ${cli}. Run 'npm run build' first.`);
|
|
68
|
+
}
|
|
69
|
+
const bind = (0, server_1.resolveBind)(cfg, { bind: opts.bind });
|
|
70
|
+
const port = pickPort(cfg, opts.port);
|
|
71
|
+
// Template lives next to package root: dist/daemon.js -> ../com.soup-nazi...
|
|
72
|
+
const templatePath = path.join(__dirname, '..', 'com.soup-nazi.snazi-serve.plist');
|
|
73
|
+
if (!fs.existsSync(templatePath)) {
|
|
74
|
+
throw new Error(`Plist template not found at ${templatePath}.`);
|
|
75
|
+
}
|
|
76
|
+
const logDir = path.join(os.homedir(), 'Library', 'Logs');
|
|
77
|
+
const filled = fs
|
|
78
|
+
.readFileSync(templatePath, 'utf8')
|
|
79
|
+
.replace(/__NODE__/g, node)
|
|
80
|
+
.replace(/__CLI__/g, cli)
|
|
81
|
+
.replace(/__BIND__/g, bind)
|
|
82
|
+
.replace(/__PORT__/g, String(port))
|
|
83
|
+
.replace(/__LOGDIR__/g, logDir);
|
|
84
|
+
const agentsDir = path.join(os.homedir(), 'Library', 'LaunchAgents');
|
|
85
|
+
fs.mkdirSync(agentsDir, { recursive: true });
|
|
86
|
+
const plistPath = path.join(agentsDir, `${exports.LABEL}.plist`);
|
|
87
|
+
fs.writeFileSync(plistPath, filled, { mode: 0o644 });
|
|
88
|
+
return { plistPath, bind, port, node, cli };
|
|
89
|
+
}
|
package/dist/doctor.js
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runDoctor = runDoctor;
|
|
4
|
+
/**
|
|
5
|
+
* `snazi doctor` — one command that tells a new user exactly what's wrong.
|
|
6
|
+
*
|
|
7
|
+
* Checks, in order:
|
|
8
|
+
* - Node version (>= 18, required for global fetch)
|
|
9
|
+
* - Config present + valid (apiUrl + token)
|
|
10
|
+
* - Server reachable with the configured token
|
|
11
|
+
* - For each configured channel: is there a local adapter, and can it read on
|
|
12
|
+
* THIS machine right now (platform + Full Disk Access for iMessage)?
|
|
13
|
+
*
|
|
14
|
+
* Output is JSON (like the rest of the CLI). `problems` are hard failures that
|
|
15
|
+
* set a non-zero exit code; `warnings` (server unreachable, a channel that only
|
|
16
|
+
* works remotely on this OS) are surfaced but do not fail, because remote-only
|
|
17
|
+
* use (e.g. a Windows box driving a Mac `snazi serve`) is legitimate.
|
|
18
|
+
*/
|
|
19
|
+
const config_1 = require("./config");
|
|
20
|
+
const api_1 = require("./api");
|
|
21
|
+
const channels_1 = require("./channels");
|
|
22
|
+
const MIN_NODE_MAJOR = 18;
|
|
23
|
+
async function runDoctor() {
|
|
24
|
+
const problems = [];
|
|
25
|
+
const warnings = [];
|
|
26
|
+
const nodeMajor = Number(process.versions.node.split('.')[0]);
|
|
27
|
+
const nodeOk = Number.isFinite(nodeMajor) && nodeMajor >= MIN_NODE_MAJOR;
|
|
28
|
+
if (!nodeOk) {
|
|
29
|
+
problems.push(`Node ${process.versions.node} is too old; snazi needs Node ${MIN_NODE_MAJOR}+ (global fetch).`);
|
|
30
|
+
}
|
|
31
|
+
const cfg = (0, config_1.readConfigIfPresent)();
|
|
32
|
+
const configPresent = Boolean(cfg);
|
|
33
|
+
const configValid = Boolean(cfg && cfg.apiUrl && cfg.apiKey);
|
|
34
|
+
if (!configPresent) {
|
|
35
|
+
problems.push(`No config at ${config_1.CONFIG_PATH}. Run 'snazi init'.`);
|
|
36
|
+
}
|
|
37
|
+
else if (!configValid) {
|
|
38
|
+
problems.push(`Config at ${config_1.CONFIG_PATH} is missing apiUrl or apiKey. Run 'snazi init'.`);
|
|
39
|
+
}
|
|
40
|
+
let serverReachable = null;
|
|
41
|
+
if (configValid && cfg) {
|
|
42
|
+
serverReachable = await (0, api_1.ping)(cfg);
|
|
43
|
+
if (!serverReachable) {
|
|
44
|
+
warnings.push(`Cannot reach ${cfg.apiUrl} with the configured token. Check the URL, token, and network.`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const configuredChannels = cfg?.channels?.length
|
|
48
|
+
? cfg.channels
|
|
49
|
+
: configValid
|
|
50
|
+
? ['imessage']
|
|
51
|
+
: [];
|
|
52
|
+
const channels = configuredChannels.map((id) => {
|
|
53
|
+
const adapter = (0, channels_1.getAdapter)(id);
|
|
54
|
+
if (!adapter) {
|
|
55
|
+
warnings.push(`Channel '${id}' has no local adapter on this build; only remote-* (against a host that supports it) will work here.`);
|
|
56
|
+
return {
|
|
57
|
+
id,
|
|
58
|
+
known: false,
|
|
59
|
+
available: false,
|
|
60
|
+
reason: 'no local adapter on this build',
|
|
61
|
+
detail: null,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
const av = adapter.availability();
|
|
65
|
+
if (!av.available) {
|
|
66
|
+
warnings.push(`Channel '${id}' is not readable locally: ${av.reason}`);
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
id,
|
|
70
|
+
known: true,
|
|
71
|
+
available: av.available,
|
|
72
|
+
reason: av.reason ?? null,
|
|
73
|
+
detail: av.detail ?? null,
|
|
74
|
+
};
|
|
75
|
+
});
|
|
76
|
+
const report = {
|
|
77
|
+
ok: problems.length === 0,
|
|
78
|
+
platform: `${process.platform}/${process.arch}`,
|
|
79
|
+
node: process.versions.node,
|
|
80
|
+
node_ok: nodeOk,
|
|
81
|
+
config: {
|
|
82
|
+
path: config_1.CONFIG_PATH,
|
|
83
|
+
present: configPresent,
|
|
84
|
+
valid: configValid,
|
|
85
|
+
apiUrl: cfg?.apiUrl ?? null,
|
|
86
|
+
token_present: Boolean(cfg?.apiKey),
|
|
87
|
+
},
|
|
88
|
+
server_reachable: serverReachable,
|
|
89
|
+
channels,
|
|
90
|
+
adapters: (0, channels_1.listAdapters)().map((a) => ({
|
|
91
|
+
id: a.id,
|
|
92
|
+
display_name: a.displayName,
|
|
93
|
+
platforms: a.platforms,
|
|
94
|
+
})),
|
|
95
|
+
problems,
|
|
96
|
+
warnings,
|
|
97
|
+
};
|
|
98
|
+
return { code: problems.length === 0 ? 0 : 1, report };
|
|
99
|
+
}
|
package/dist/init.js
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.runInit = runInit;
|
|
37
|
+
/**
|
|
38
|
+
* `snazi init` — create or update ~/.snazi/config.json without hand-editing JSON.
|
|
39
|
+
*
|
|
40
|
+
* Works two ways:
|
|
41
|
+
* - Interactive (a TTY): prompts for the deployment URL, the account READ
|
|
42
|
+
* token, and the channel(s), each pre-filled with a sensible default.
|
|
43
|
+
* - Non-interactive (agents/CI, or `--yes`): takes values from flags
|
|
44
|
+
* (--api-url, --token, --channel) and/or an existing config. Prompts are
|
|
45
|
+
* skipped; a missing token is a hard error with guidance.
|
|
46
|
+
*
|
|
47
|
+
* Existing config is treated as defaults and MERGED (serve/remote keys are
|
|
48
|
+
* preserved), so re-running init never clobbers unrelated settings. Prompts are
|
|
49
|
+
* written to stderr so stdout stays clean JSON for scripted callers.
|
|
50
|
+
*/
|
|
51
|
+
const readline = __importStar(require("node:readline/promises"));
|
|
52
|
+
const config_1 = require("./config");
|
|
53
|
+
/** Normalize a user-typed base URL: trim, drop trailing slash, default https. */
|
|
54
|
+
function normalizeUrl(input) {
|
|
55
|
+
let u = (input ?? '').trim().replace(/\/+$/, '');
|
|
56
|
+
if (!u)
|
|
57
|
+
return '';
|
|
58
|
+
if (!/^https?:\/\//i.test(u))
|
|
59
|
+
u = `https://${u}`;
|
|
60
|
+
return u;
|
|
61
|
+
}
|
|
62
|
+
function maskToken(t) {
|
|
63
|
+
if (!t)
|
|
64
|
+
return '';
|
|
65
|
+
return `${t.slice(0, 6)}…(${t.length})`;
|
|
66
|
+
}
|
|
67
|
+
async function runInit(a) {
|
|
68
|
+
const existing = (0, config_1.readConfigIfPresent)();
|
|
69
|
+
const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY) && !a.yes;
|
|
70
|
+
const noFlags = !a.apiUrl && !a.token && !a.channel;
|
|
71
|
+
// Existing config + nothing to change + can't prompt -> no-op (don't clobber).
|
|
72
|
+
if (existing && !a.force && !interactive && noFlags) {
|
|
73
|
+
return {
|
|
74
|
+
code: 0,
|
|
75
|
+
result: {
|
|
76
|
+
ok: true,
|
|
77
|
+
note: `Config already present at ${config_1.CONFIG_PATH}. Pass --force to rewrite, flags (--api-url/--token/--channel) to update, or run interactively.`,
|
|
78
|
+
config_path: config_1.CONFIG_PATH,
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
let rl;
|
|
83
|
+
if (interactive) {
|
|
84
|
+
rl = readline.createInterface({
|
|
85
|
+
input: process.stdin,
|
|
86
|
+
output: process.stderr,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
const ask = async (q, def) => {
|
|
90
|
+
if (!rl)
|
|
91
|
+
return def ?? '';
|
|
92
|
+
const suffix = def ? ` [${def}]` : '';
|
|
93
|
+
const ans = (await rl.question(`${q}${suffix}: `)).trim();
|
|
94
|
+
return ans || def || '';
|
|
95
|
+
};
|
|
96
|
+
try {
|
|
97
|
+
const apiUrl = normalizeUrl(a.apiUrl ??
|
|
98
|
+
(interactive
|
|
99
|
+
? await ask('Deployment URL', existing?.apiUrl ?? config_1.DEFAULT_API_URL)
|
|
100
|
+
: existing?.apiUrl ?? config_1.DEFAULT_API_URL));
|
|
101
|
+
const token = (a.token ??
|
|
102
|
+
(interactive
|
|
103
|
+
? await ask('Account READ token (from the dashboard Account page)', existing?.apiKey)
|
|
104
|
+
: existing?.apiKey ?? '')).trim();
|
|
105
|
+
const channelsDefault = existing?.channels?.length
|
|
106
|
+
? existing.channels.join(',')
|
|
107
|
+
: 'imessage';
|
|
108
|
+
const channelsRaw = a.channel ?? (interactive ? await ask('Channel(s), comma-separated', channelsDefault) : channelsDefault);
|
|
109
|
+
const channels = channelsRaw
|
|
110
|
+
.split(',')
|
|
111
|
+
.map((s) => s.trim())
|
|
112
|
+
.filter(Boolean);
|
|
113
|
+
if (!apiUrl) {
|
|
114
|
+
return { code: 2, result: { error: 'A deployment URL is required.' } };
|
|
115
|
+
}
|
|
116
|
+
if (!token) {
|
|
117
|
+
return {
|
|
118
|
+
code: 2,
|
|
119
|
+
result: {
|
|
120
|
+
error: 'A READ token is required. Pass --token <token> (or run interactively). ' +
|
|
121
|
+
'Get it from your deployment dashboard → Account page.',
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
const merged = {
|
|
126
|
+
...(existing ?? {}),
|
|
127
|
+
apiUrl,
|
|
128
|
+
apiKey: token,
|
|
129
|
+
channels: channels.length ? channels : ['imessage'],
|
|
130
|
+
};
|
|
131
|
+
(0, config_1.saveConfig)(merged);
|
|
132
|
+
const warnings = [];
|
|
133
|
+
if (token.length < 16) {
|
|
134
|
+
warnings.push('That token looks short — double-check you copied the full READ token.');
|
|
135
|
+
}
|
|
136
|
+
return {
|
|
137
|
+
code: 0,
|
|
138
|
+
result: {
|
|
139
|
+
ok: true,
|
|
140
|
+
config_path: config_1.CONFIG_PATH,
|
|
141
|
+
apiUrl,
|
|
142
|
+
apiKey: maskToken(token),
|
|
143
|
+
channels: merged.channels,
|
|
144
|
+
warnings: warnings.length ? warnings : undefined,
|
|
145
|
+
next_steps: [
|
|
146
|
+
'snazi doctor # verify config, connectivity, and channel access',
|
|
147
|
+
'snazi list-new --since 120',
|
|
148
|
+
],
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
finally {
|
|
153
|
+
rl?.close();
|
|
154
|
+
}
|
|
155
|
+
}
|