gipity 1.0.399 → 1.0.401
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/dist/claude-setup.js +87 -0
- package/dist/commands/claude.js +29 -10
- package/dist/commands/doctor.js +87 -4
- package/dist/commands/payments.js +54 -0
- package/dist/commands/relay-install.js +21 -41
- package/dist/commands/relay.js +91 -3
- package/dist/index.js +2 -1
- package/dist/knowledge.js +4 -1
- package/dist/relay/onboarding.js +16 -65
- package/dist/relay/setup.js +142 -0
- package/package.json +2 -2
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Code install + detection helpers, shared by `gipity claude` (which
|
|
3
|
+
* auto-ensures Claude Code before launching) and `gipity doctor` (which reports
|
|
4
|
+
* Claude state). Centralizing here means a GUI/installer — e.g. the desktop
|
|
5
|
+
* onboarding client — drives Claude setup through the CLI instead of
|
|
6
|
+
* re-implementing it.
|
|
7
|
+
*
|
|
8
|
+
* The plan (`claudeInstallPlan`) is pure + unit-tested; actually running
|
|
9
|
+
* `which`/`npm` happens in the helpers below and in the command layer.
|
|
10
|
+
*/
|
|
11
|
+
import { execSync } from 'child_process';
|
|
12
|
+
import { existsSync } from 'fs';
|
|
13
|
+
import { homedir } from 'os';
|
|
14
|
+
import { join } from 'path';
|
|
15
|
+
export const CLAUDE_PACKAGE = '@anthropic-ai/claude-code';
|
|
16
|
+
/** Pure: the platform-appropriate detect + install commands. Unit-tested. */
|
|
17
|
+
export function claudeInstallPlan(platformOverride) {
|
|
18
|
+
const plat = platformOverride ?? process.platform;
|
|
19
|
+
return {
|
|
20
|
+
checkCmd: plat === 'win32' ? 'where claude' : 'which claude',
|
|
21
|
+
installArgv: ['npm', 'install', '-g', CLAUDE_PACKAGE],
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
/** Whether the `claude` binary resolves on PATH. */
|
|
25
|
+
export function isClaudeInstalled(platformOverride) {
|
|
26
|
+
try {
|
|
27
|
+
execSync(claudeInstallPlan(platformOverride).checkCmd, { stdio: 'ignore' });
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Best-effort, positive-evidence-only check for "Claude Code is logged in."
|
|
36
|
+
*
|
|
37
|
+
* NOTE: on macOS the OAuth token can live in the Keychain, which we can't read
|
|
38
|
+
* non-interactively — so a `false` here may be a false-negative. Callers should
|
|
39
|
+
* treat `false` as "offer the login step" rather than "definitely logged out."
|
|
40
|
+
* (Tracked as an open question in the desktop onboarding spec.)
|
|
41
|
+
*/
|
|
42
|
+
export function isClaudeAuthenticated() {
|
|
43
|
+
if (process.env.CLAUDE_CODE_OAUTH_TOKEN || process.env.ANTHROPIC_API_KEY)
|
|
44
|
+
return true;
|
|
45
|
+
return existsSync(join(homedir(), '.claude', '.credentials.json'));
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Definitive auth check: actually run a tiny headless `claude -p` ping. If it
|
|
49
|
+
* returns output, Claude Code is authenticated. This is the reliable check
|
|
50
|
+
* (unlike `isClaudeAuthenticated`'s file/env heuristic) but it's **billed** (a
|
|
51
|
+
* real LLM call) and slow (network + model latency), so it's opt-in — use it at
|
|
52
|
+
* a decision point (e.g. confirming the login step took), not on every poll.
|
|
53
|
+
*/
|
|
54
|
+
export function probeClaudeAuthenticated() {
|
|
55
|
+
if (!isClaudeInstalled())
|
|
56
|
+
return false;
|
|
57
|
+
try {
|
|
58
|
+
const out = execSync('claude -p "Reply with the single word: PONG"', {
|
|
59
|
+
timeout: 60_000,
|
|
60
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
61
|
+
encoding: 'utf-8',
|
|
62
|
+
});
|
|
63
|
+
return typeof out === 'string' && out.trim().length > 0;
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
// Non-zero exit (unauthenticated, network error, etc.) → treat as not auth'd.
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Install Claude Code via npm if it isn't already on PATH. Idempotent: a no-op
|
|
72
|
+
* (beyond the PATH check) when already present, unless `force`. `quiet`
|
|
73
|
+
* suppresses npm's output (for headless/GUI callers); default streams it.
|
|
74
|
+
*/
|
|
75
|
+
export function ensureClaudeInstalled(opts = {}) {
|
|
76
|
+
if (!opts.force && isClaudeInstalled())
|
|
77
|
+
return { installed: true, alreadyPresent: true };
|
|
78
|
+
const { installArgv } = claudeInstallPlan();
|
|
79
|
+
try {
|
|
80
|
+
execSync(installArgv.join(' '), { stdio: opts.quiet ? 'ignore' : 'inherit' });
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
// Fall through to a definitive PATH re-check rather than trusting the throw.
|
|
84
|
+
}
|
|
85
|
+
return { installed: isClaudeInstalled(), alreadyPresent: false };
|
|
86
|
+
}
|
|
87
|
+
//# sourceMappingURL=claude-setup.js.map
|
package/dist/commands/claude.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import { join, dirname, resolve, basename } from 'path';
|
|
3
3
|
import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync, readdirSync, statSync } from 'fs';
|
|
4
|
-
import {
|
|
4
|
+
import { spawn } from 'child_process';
|
|
5
5
|
import { homedir } from 'os';
|
|
6
6
|
import { fileURLToPath } from 'url';
|
|
7
7
|
import { resolveCommand } from '../platform.js';
|
|
@@ -18,6 +18,7 @@ import { brand, bold, info, success, warning, error as clrError, muted } from '.
|
|
|
18
18
|
import { createProgressReporter } from '../progress.js';
|
|
19
19
|
import { printBanner } from '../banner.js';
|
|
20
20
|
import { scanForAdoption, isLikelyEmpty, canAdoptCwd, formatCwdLabel, formatBytes, adoptCurrentDir, ADOPT_THRESHOLDS, } from '../adopt-cwd.js';
|
|
21
|
+
import { isClaudeInstalled, ensureClaudeInstalled, CLAUDE_PACKAGE } from '../claude-setup.js';
|
|
21
22
|
const __clDir = dirname(fileURLToPath(import.meta.url));
|
|
22
23
|
const __clPkg = JSON.parse(readFileSync(resolve(__clDir, '../../package.json'), 'utf-8'));
|
|
23
24
|
/** Report a sync run to the user. Beyond the applied-changes line, this SURFACES
|
|
@@ -246,6 +247,21 @@ export const claudeCommand = new Command('claude')
|
|
|
246
247
|
// a human at the terminal. Requires an existing .gipity.json - we
|
|
247
248
|
// can't interactively pick or create a project in this mode.
|
|
248
249
|
const rawArgs = process.argv.slice(process.argv.indexOf('claude') + 1);
|
|
250
|
+
// `gipity claude install` - explicit, GUI-callable Claude Code install.
|
|
251
|
+
// Handled here (leading positional) rather than as a Commander subcommand
|
|
252
|
+
// because this command uses allowUnknownOption/allowExcessArguments to
|
|
253
|
+
// pass everything through to `claude`; a real subcommand would risk that
|
|
254
|
+
// passthrough (which the relay daemon's `gipity claude -p` depends on).
|
|
255
|
+
if (rawArgs[0] === 'install') {
|
|
256
|
+
const r = ensureClaudeInstalled({ force: rawArgs.includes('--force') });
|
|
257
|
+
if (r.alreadyPresent)
|
|
258
|
+
console.log(` ${success('Claude Code already installed.')}`);
|
|
259
|
+
else if (r.installed)
|
|
260
|
+
console.log(` ${success('Claude Code installed.')}`);
|
|
261
|
+
else
|
|
262
|
+
console.log(` ${clrError('Could not install Claude Code.')} Install manually: npm install -g ${CLAUDE_PACKAGE}`);
|
|
263
|
+
process.exit(r.installed ? 0 : 1);
|
|
264
|
+
}
|
|
249
265
|
const nonInteractive = rawArgs.some(a => a === '-p' || a === '--print' || a.startsWith('--print=') || a.startsWith('-p='));
|
|
250
266
|
// Headless progress emitter: routes to stderr (stdout stays clean for
|
|
251
267
|
// the child's result), drops the interactive 2-space indent, collapses
|
|
@@ -608,15 +624,18 @@ export const claudeCommand = new Command('claude')
|
|
|
608
624
|
console.log(` Done. cd ${process.cwd()} && gipity claude`);
|
|
609
625
|
return;
|
|
610
626
|
}
|
|
611
|
-
//
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
627
|
+
// Ensure Claude Code is installed - install it ourselves if missing so a
|
|
628
|
+
// GUI/installer (and terminal users) get it for free, rather than being
|
|
629
|
+
// told to run npm by hand.
|
|
630
|
+
if (!isClaudeInstalled()) {
|
|
631
|
+
console.log(' Claude Code not found - installing it now...');
|
|
632
|
+
const r = ensureClaudeInstalled();
|
|
633
|
+
if (!r.installed) {
|
|
634
|
+
console.log(` ${clrError('Could not install Claude Code.')} Install manually: npm install -g ${CLAUDE_PACKAGE}`);
|
|
635
|
+
console.log(` Then: cd ${process.cwd()} && claude`);
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
console.log(` ${success('Claude Code installed.')}`);
|
|
620
639
|
}
|
|
621
640
|
// Ensure the Gipity plugin is actually installed at user scope (not just
|
|
622
641
|
// enabled declaratively) so its capture + file-sync hooks load in this
|
package/dist/commands/doctor.js
CHANGED
|
@@ -3,6 +3,12 @@ import { existsSync, readFileSync, statSync } from 'fs';
|
|
|
3
3
|
import { join } from 'path';
|
|
4
4
|
import { LOCAL_PKG_DIR, LOCAL_ENTRY, STATE_FILE, SETTINGS_FILE, UPDATE_LOG, readState, readSettings, updatesDisabled } from '../updater/state.js';
|
|
5
5
|
import { bold, dim, success, warning, error as clrError, muted } from '../colors.js';
|
|
6
|
+
import { getAuth, sessionExpired } from '../auth.js';
|
|
7
|
+
import { isClaudeInstalled, isClaudeAuthenticated, probeClaudeAuthenticated } from '../claude-setup.js';
|
|
8
|
+
import * as relayState from '../relay/state.js';
|
|
9
|
+
import { planFor, UnsupportedPlatformError } from '../relay/installers.js';
|
|
10
|
+
import { resolveCliPath } from '../relay/setup.js';
|
|
11
|
+
const NODE_MIN_MAJOR = 18;
|
|
6
12
|
function localVersion() {
|
|
7
13
|
const pkgPath = join(LOCAL_PKG_DIR, 'package.json');
|
|
8
14
|
if (!existsSync(pkgPath))
|
|
@@ -38,16 +44,94 @@ function rel(t) {
|
|
|
38
44
|
return `${Math.floor(s / 3600)}h ago`;
|
|
39
45
|
return `${Math.floor(s / 86400)}d ago`;
|
|
40
46
|
}
|
|
47
|
+
/** Whether the relay's OS login-service unit file exists. Reflects "autostart
|
|
48
|
+
* has been installed" (by `relay setup`/`relay install`); cheap and poll-safe.
|
|
49
|
+
* null on an unsupported platform. */
|
|
50
|
+
function relayAutostartInstalled() {
|
|
51
|
+
try {
|
|
52
|
+
return existsSync(planFor({ cliPath: resolveCliPath() }).path);
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
if (err instanceof UnsupportedPlatformError)
|
|
56
|
+
return null;
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Snapshot of the local environment a GUI/installer needs to drive onboarding:
|
|
62
|
+
* Node, the Gipity CLI + login state, Claude Code install + auth, and the relay
|
|
63
|
+
* pairing/daemon state. The single "state of the world" the desktop onboarding
|
|
64
|
+
* client polls (`gipity doctor --json`).
|
|
65
|
+
*/
|
|
66
|
+
export function gatherEnv(opts = {}) {
|
|
67
|
+
const nodeMajor = parseInt(process.versions.node.split('.')[0], 10) || 0;
|
|
68
|
+
const auth = getAuth();
|
|
69
|
+
const expired = auth ? sessionExpired() : false;
|
|
70
|
+
const device = relayState.getDevice();
|
|
71
|
+
const node = { ok: nodeMajor >= NODE_MIN_MAJOR, version: process.versions.node };
|
|
72
|
+
const gipity = {
|
|
73
|
+
installed: true, // we're running it
|
|
74
|
+
version: shimVersion(),
|
|
75
|
+
logged_in: !!auth && !expired,
|
|
76
|
+
email: auth?.email ?? null,
|
|
77
|
+
session_expired: expired,
|
|
78
|
+
};
|
|
79
|
+
const claude = { installed: isClaudeInstalled(), authenticated: false };
|
|
80
|
+
// Default: cheap heuristic (poll-safe). With --probe-claude: a real (billed)
|
|
81
|
+
// `claude -p` ping for a definitive answer.
|
|
82
|
+
claude.authenticated = claude.installed && (opts.probeClaude ? probeClaudeAuthenticated() : isClaudeAuthenticated());
|
|
83
|
+
const relay = {
|
|
84
|
+
paired: !!device,
|
|
85
|
+
running: relayState.isDaemonRunning(),
|
|
86
|
+
paused: relayState.isPaused(),
|
|
87
|
+
autostart: relayAutostartInstalled(),
|
|
88
|
+
device: device ? { name: device.name, guid: device.guid } : null,
|
|
89
|
+
};
|
|
90
|
+
const dis = updatesDisabled();
|
|
91
|
+
const updState = readState();
|
|
92
|
+
const cli = {
|
|
93
|
+
shim_version: shimVersion(),
|
|
94
|
+
local_version: localVersion(),
|
|
95
|
+
local_install_ok: existsSync(LOCAL_ENTRY),
|
|
96
|
+
auto_updates: !dis.disabled,
|
|
97
|
+
updates_disabled_reason: dis.disabled ? (dis.reason ?? null) : null,
|
|
98
|
+
last_check_at: updState.lastCheckAt,
|
|
99
|
+
last_error: updState.lastError,
|
|
100
|
+
};
|
|
101
|
+
const ready = node.ok && gipity.logged_in && claude.installed && claude.authenticated && relay.paired && relay.running;
|
|
102
|
+
return { ready, node, gipity, claude, relay, cli };
|
|
103
|
+
}
|
|
104
|
+
function yn(v) {
|
|
105
|
+
return v ? success('yes') : warning('no');
|
|
106
|
+
}
|
|
41
107
|
export const doctorCommand = new Command('doctor')
|
|
42
|
-
.description('Check install health')
|
|
43
|
-
.
|
|
108
|
+
.description('Check install + environment health')
|
|
109
|
+
.option('--json', 'Machine-readable environment report (for installers/GUIs)')
|
|
110
|
+
.option('--probe-claude', 'Verify Claude Code auth with a real (billed) `claude -p` ping instead of the cheap heuristic')
|
|
111
|
+
.action((opts) => {
|
|
112
|
+
const env = gatherEnv({ probeClaude: opts.probeClaude });
|
|
113
|
+
if (opts.json) {
|
|
114
|
+
console.log(JSON.stringify(env, null, 2));
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
// ── Environment (what onboarding cares about) ──────────────────────
|
|
118
|
+
console.log(bold('Gipity - doctor'));
|
|
119
|
+
console.log('');
|
|
120
|
+
console.log(bold('Environment'));
|
|
121
|
+
console.log(`${muted('node ')} ${env.node.version} ${env.node.ok ? success('✓') : clrError(`(need ${NODE_MIN_MAJOR}+)`)}`);
|
|
122
|
+
console.log(`${muted('gipity login ')} ${env.gipity.logged_in ? success(`logged in as ${env.gipity.email}`) : (env.gipity.session_expired ? warning(`session expired (${env.gipity.email})`) : warning('not logged in'))}`);
|
|
123
|
+
console.log(`${muted('claude code ')} installed ${yn(env.claude.installed)} · authenticated ${yn(env.claude.authenticated)}`);
|
|
124
|
+
const autostartLabel = env.relay.autostart === null ? muted('n/a') : yn(env.relay.autostart);
|
|
125
|
+
console.log(`${muted('relay ')} paired ${yn(env.relay.paired)} · running ${yn(env.relay.running)} · autostart ${autostartLabel}${env.relay.paused ? warning(' · paused') : ''}${env.relay.device ? muted(` (${env.relay.device.name})`) : ''}`);
|
|
126
|
+
console.log(`${muted('ready ')} ${env.ready ? success('yes') : warning('no - run `gipity claude` (or the desktop app) to finish setup')}`);
|
|
127
|
+
// ── CLI install / update health ────────────────────────────────────
|
|
44
128
|
const state = readState();
|
|
45
129
|
const settings = readSettings();
|
|
46
130
|
const dis = updatesDisabled();
|
|
47
131
|
const local = localVersion();
|
|
48
132
|
const localOk = existsSync(LOCAL_ENTRY);
|
|
49
|
-
console.log(bold('Gipity CLI - doctor'));
|
|
50
133
|
console.log('');
|
|
134
|
+
console.log(bold('CLI install'));
|
|
51
135
|
console.log(`${muted('shim version ')} ${shimVersion()}`);
|
|
52
136
|
console.log(`${muted('local version ')} ${local ?? dim('not installed')} ${localOk ? success('✓') : warning('(running from shim fallback)')}`);
|
|
53
137
|
console.log(`${muted('local install ')} ${LOCAL_PKG_DIR}`);
|
|
@@ -60,6 +144,5 @@ export const doctorCommand = new Command('doctor')
|
|
|
60
144
|
console.log(`${muted('update log ')} ${existsSync(UPDATE_LOG) ? `${UPDATE_LOG} (${statSync(UPDATE_LOG).size} bytes)` : dim('(none yet)')}`);
|
|
61
145
|
console.log('');
|
|
62
146
|
console.log(dim('Force an update with: gipity update'));
|
|
63
|
-
console.log(dim('Disable auto-update: export DISABLE_AUTOUPDATER=1 (or set autoUpdates: false in settings.json)'));
|
|
64
147
|
});
|
|
65
148
|
//# sourceMappingURL=doctor.js.map
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { get, post } from '../api.js';
|
|
3
|
+
import { requireConfig } from '../config.js';
|
|
4
|
+
import { brand, bold, dim, muted } from '../colors.js';
|
|
5
|
+
import { run } from '../helpers/index.js';
|
|
6
|
+
function renderStatus(status) {
|
|
7
|
+
if (!status.connected) {
|
|
8
|
+
console.log('Stripe: not connected. Run `gipity payments connect` to set it up.');
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
const charges = status.charges_enabled ? brand('enabled') : dim('disabled');
|
|
12
|
+
const payouts = status.payouts_enabled ? brand('enabled') : dim('disabled');
|
|
13
|
+
console.log(`Stripe: ${bold('connected')}`);
|
|
14
|
+
console.log(` Charges: ${charges}`);
|
|
15
|
+
console.log(` Payouts: ${payouts}`);
|
|
16
|
+
console.log(` Onboarding: ${status.details_submitted ? 'complete' : 'incomplete'}`);
|
|
17
|
+
if (!status.charges_enabled) {
|
|
18
|
+
console.log(muted('\nCharges are not enabled yet — finish Stripe onboarding via `gipity payments connect`.'));
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export const paymentsCommand = new Command('payments')
|
|
22
|
+
.description('Connect Stripe so your app can charge its users (one-time + subscriptions)');
|
|
23
|
+
paymentsCommand
|
|
24
|
+
.command('connect')
|
|
25
|
+
.description('Start (or resume) Stripe onboarding for this app — prints a link to finish in your browser')
|
|
26
|
+
.option('--return-url <url>', 'Where Stripe redirects after onboarding')
|
|
27
|
+
.option('--json', 'Output as JSON')
|
|
28
|
+
.action((opts) => run('Payments', async () => {
|
|
29
|
+
const config = requireConfig();
|
|
30
|
+
const body = opts.returnUrl ? { returnUrl: opts.returnUrl } : {};
|
|
31
|
+
const res = await post(`/api/${config.projectGuid}/services/payments/connect`, body);
|
|
32
|
+
if (opts.json) {
|
|
33
|
+
console.log(JSON.stringify(res.data));
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
console.log('Open this link to connect your Stripe account (bank + identity; no API keys to paste):\n');
|
|
37
|
+
console.log(brand(res.data.url));
|
|
38
|
+
console.log(muted('\nMoney lands in your Stripe account; Gipity takes a small platform fee.'));
|
|
39
|
+
console.log(muted('When done, run `gipity payments status` to confirm charges are enabled.'));
|
|
40
|
+
}));
|
|
41
|
+
paymentsCommand
|
|
42
|
+
.command('status', { isDefault: true })
|
|
43
|
+
.description('Show whether this app can take payments yet')
|
|
44
|
+
.option('--json', 'Output as JSON')
|
|
45
|
+
.action((opts) => run('Payments', async () => {
|
|
46
|
+
const config = requireConfig();
|
|
47
|
+
const res = await get(`/api/${config.projectGuid}/services/payments/status`);
|
|
48
|
+
if (opts.json) {
|
|
49
|
+
console.log(JSON.stringify(res.data));
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
renderStatus(res.data);
|
|
53
|
+
}));
|
|
54
|
+
//# sourceMappingURL=payments.js.map
|
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
import { spawnSync } from 'child_process';
|
|
2
|
-
import { resolve, dirname } from 'path';
|
|
3
|
-
import { mkdirSync, writeFileSync } from 'fs';
|
|
4
1
|
import { confirm } from '../utils.js';
|
|
5
2
|
import { bold, dim, success, error as clrError, muted, info } from '../colors.js';
|
|
6
3
|
import * as state from '../relay/state.js';
|
|
7
4
|
import { planFor, UnsupportedPlatformError } from '../relay/installers.js';
|
|
5
|
+
import { installAutostart, removeAutostart, resolveCliPath } from '../relay/setup.js';
|
|
8
6
|
function requirePaired() {
|
|
9
7
|
const device = state.getDevice();
|
|
10
8
|
if (!device) {
|
|
@@ -13,27 +11,6 @@ function requirePaired() {
|
|
|
13
11
|
}
|
|
14
12
|
return device;
|
|
15
13
|
}
|
|
16
|
-
/** Absolute path to the currently-running `gipity` CLI. Embedded in the
|
|
17
|
-
* service unit so the launchd/systemd/Task Scheduler entry re-launches
|
|
18
|
-
* the same binary even if PATH changes. */
|
|
19
|
-
function resolveCliPath() {
|
|
20
|
-
return resolve(process.argv[1] ?? 'gipity');
|
|
21
|
-
}
|
|
22
|
-
/** Run a sequence of argv commands directly (no shell). Returns true if all
|
|
23
|
-
* succeeded. Spawns each command's stdio inherited so the user sees the
|
|
24
|
-
* service manager's output verbatim. */
|
|
25
|
-
function runArgvSequence(cmds, { failFast }) {
|
|
26
|
-
let allOk = true;
|
|
27
|
-
for (const argv of cmds) {
|
|
28
|
-
const r = spawnSync(argv[0], argv.slice(1), { stdio: 'inherit' });
|
|
29
|
-
if (r.status !== 0) {
|
|
30
|
-
allOk = false;
|
|
31
|
-
if (failFast)
|
|
32
|
-
return false;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
return allOk;
|
|
36
|
-
}
|
|
37
14
|
export function registerInstallCommands(relayCommand) {
|
|
38
15
|
relayCommand
|
|
39
16
|
.command('install')
|
|
@@ -41,10 +18,9 @@ export function registerInstallCommands(relayCommand) {
|
|
|
41
18
|
.option('--print', 'Print the service-unit file and the commands, but don\'t run them')
|
|
42
19
|
.action(async (opts) => {
|
|
43
20
|
requirePaired();
|
|
44
|
-
const cliPath = resolveCliPath();
|
|
45
21
|
let plan;
|
|
46
22
|
try {
|
|
47
|
-
plan = planFor({ cliPath });
|
|
23
|
+
plan = planFor({ cliPath: resolveCliPath() });
|
|
48
24
|
}
|
|
49
25
|
catch (err) {
|
|
50
26
|
if (err instanceof UnsupportedPlatformError) {
|
|
@@ -69,10 +45,11 @@ export function registerInstallCommands(relayCommand) {
|
|
|
69
45
|
console.log(muted('Cancelled. (Use --print to preview without installing.)'));
|
|
70
46
|
return;
|
|
71
47
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
48
|
+
// Shared helper writes the unit + runs the enable commands; `inherit`
|
|
49
|
+
// surfaces launchctl/systemctl/schtasks output for this interactive path.
|
|
50
|
+
const res = installAutostart({ stdio: 'inherit' });
|
|
51
|
+
console.log(success(`Wrote ${res.path}`));
|
|
52
|
+
if (!res.ok) {
|
|
76
53
|
console.error(clrError(`Couldn't enable autostart. Try manually: ${plan.enableDisplay}`));
|
|
77
54
|
process.exit(1);
|
|
78
55
|
}
|
|
@@ -89,9 +66,21 @@ export function registerInstallCommands(relayCommand) {
|
|
|
89
66
|
console.error(clrError('Usage: gipity relay autostart <on|off>'));
|
|
90
67
|
process.exit(1);
|
|
91
68
|
}
|
|
92
|
-
let plan;
|
|
93
69
|
try {
|
|
94
|
-
|
|
70
|
+
if (want === 'on') {
|
|
71
|
+
const plan = planFor({ cliPath: resolveCliPath() });
|
|
72
|
+
console.log(`${info('Running:')} ${dim(plan.enableDisplay)}`);
|
|
73
|
+
const res = installAutostart({ stdio: 'inherit' });
|
|
74
|
+
if (!res.ok) {
|
|
75
|
+
console.error(clrError('Command failed.'));
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
// Disable is best-effort - the task may already be stopped.
|
|
81
|
+
const { plan } = removeAutostart({ stdio: 'inherit' });
|
|
82
|
+
console.log(`${info('Running:')} ${dim(plan.disableDisplay)}`);
|
|
83
|
+
}
|
|
95
84
|
}
|
|
96
85
|
catch (err) {
|
|
97
86
|
if (err instanceof UnsupportedPlatformError) {
|
|
@@ -101,15 +90,6 @@ export function registerInstallCommands(relayCommand) {
|
|
|
101
90
|
else
|
|
102
91
|
throw err;
|
|
103
92
|
}
|
|
104
|
-
const cmds = want === 'on' ? plan.enableCmds : plan.disableCmds;
|
|
105
|
-
const display = want === 'on' ? plan.enableDisplay : plan.disableDisplay;
|
|
106
|
-
console.log(`${info('Running:')} ${dim(display)}`);
|
|
107
|
-
// Disable is best-effort (the task may already be stopped); enable is fail-fast.
|
|
108
|
-
const ok = runArgvSequence(cmds, { failFast: want === 'on' });
|
|
109
|
-
if (!ok && want === 'on') {
|
|
110
|
-
console.error(clrError('Command failed.'));
|
|
111
|
-
process.exit(1);
|
|
112
|
-
}
|
|
113
93
|
console.log(success(`Autostart ${want}.`));
|
|
114
94
|
});
|
|
115
95
|
}
|
package/dist/commands/relay.js
CHANGED
|
@@ -8,14 +8,98 @@
|
|
|
8
8
|
import { Command } from 'commander';
|
|
9
9
|
import { existsSync, readFileSync, unlinkSync } from 'fs';
|
|
10
10
|
import { spawn } from 'child_process';
|
|
11
|
-
import { post } from '../api.js';
|
|
11
|
+
import { post, ApiError } from '../api.js';
|
|
12
12
|
import { confirm } from '../utils.js';
|
|
13
|
-
import { bold, brand, success, error as clrError, muted, } from '../colors.js';
|
|
13
|
+
import { bold, brand, dim, success, error as clrError, muted, } from '../colors.js';
|
|
14
14
|
import * as state from '../relay/state.js';
|
|
15
15
|
import * as daemon from '../relay/daemon.js';
|
|
16
|
+
import { UnsupportedPlatformError } from '../relay/installers.js';
|
|
17
|
+
import { pairDevice, startDaemon, installAutostart } from '../relay/setup.js';
|
|
16
18
|
import { registerInstallCommands } from './relay-install.js';
|
|
17
19
|
export const relayCommand = new Command('relay')
|
|
18
20
|
.description('Pair with the web CLI');
|
|
21
|
+
// ─── gipity relay setup ────────────────────────────────────────────────
|
|
22
|
+
// Non-interactive pair + (optionally) start + autostart, for installers and
|
|
23
|
+
// GUIs (e.g. the desktop onboarding client) that can't drive prompts. The
|
|
24
|
+
// interactive equivalent is the first-run block in `gipity claude`.
|
|
25
|
+
relayCommand
|
|
26
|
+
.command('setup')
|
|
27
|
+
.description('Pair this machine and start the relay - non-interactive (for installers/GUIs)')
|
|
28
|
+
.option('--name <name>', 'Device name shown in the web CLI (default: this machine\'s hostname)')
|
|
29
|
+
.option('--no-start', 'Pair only; do not start the relay daemon now')
|
|
30
|
+
.option('--no-autostart', 'Skip the OS login service (use when a supervising app owns the daemon)')
|
|
31
|
+
.option('--force', 'Re-pair even if already paired (revokes the old device first)')
|
|
32
|
+
.option('--json', 'Machine-readable output')
|
|
33
|
+
.action(async (opts) => {
|
|
34
|
+
const fail = (code, message) => {
|
|
35
|
+
if (opts.json) {
|
|
36
|
+
console.log(JSON.stringify({ ok: false, code, error: message }));
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
console.error(clrError(message));
|
|
40
|
+
if (code === 'not_authenticated')
|
|
41
|
+
console.error(muted('Run `gipity login` first.'));
|
|
42
|
+
}
|
|
43
|
+
process.exit(1);
|
|
44
|
+
};
|
|
45
|
+
// 1. Pair (idempotent unless --force).
|
|
46
|
+
let device;
|
|
47
|
+
try {
|
|
48
|
+
device = await pairDevice({ name: opts.name, force: opts.force });
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
if (err instanceof ApiError && err.statusCode === 401) {
|
|
52
|
+
return fail('not_authenticated', 'Not logged in - cannot pair this machine.');
|
|
53
|
+
}
|
|
54
|
+
return fail('pair_failed', `Could not pair: ${err?.message || err}`);
|
|
55
|
+
}
|
|
56
|
+
// 2. Start the daemon now (unless --no-start).
|
|
57
|
+
let daemonStarted = false;
|
|
58
|
+
if (opts.start) {
|
|
59
|
+
startDaemon();
|
|
60
|
+
daemonStarted = true;
|
|
61
|
+
}
|
|
62
|
+
// 3. Install OS autostart (unless --no-autostart). Option B desktop clients
|
|
63
|
+
// pass --no-autostart because the app itself supervises `relay run`.
|
|
64
|
+
const autostart = { requested: opts.autostart, installed: false, supported: true, summary: '' };
|
|
65
|
+
if (opts.autostart) {
|
|
66
|
+
try {
|
|
67
|
+
const res = installAutostart();
|
|
68
|
+
autostart.installed = res.ok;
|
|
69
|
+
autostart.summary = res.summary;
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
if (err instanceof UnsupportedPlatformError) {
|
|
73
|
+
autostart.supported = false;
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
return fail('autostart_failed', `Autostart install failed: ${err?.message || err}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
// 4. Report.
|
|
81
|
+
if (opts.json) {
|
|
82
|
+
console.log(JSON.stringify({
|
|
83
|
+
ok: true,
|
|
84
|
+
device: { guid: device.guid, name: device.name, platform: device.platform, reused: device.reused },
|
|
85
|
+
daemon_started: daemonStarted,
|
|
86
|
+
autostart,
|
|
87
|
+
}));
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
console.log(success(`${device.reused ? 'Already paired' : 'Paired'} as ${bold(device.name)} ${muted(`(${device.guid})`)}.`));
|
|
91
|
+
if (daemonStarted)
|
|
92
|
+
console.log(success('Relay started.'));
|
|
93
|
+
if (autostart.requested) {
|
|
94
|
+
if (!autostart.supported)
|
|
95
|
+
console.log(muted(`Auto-start not supported on ${process.platform}; skipped.`));
|
|
96
|
+
else if (autostart.installed)
|
|
97
|
+
console.log(`${success('Auto-start installed.')} ${dim(autostart.summary)}`);
|
|
98
|
+
else
|
|
99
|
+
console.log(muted('Auto-start enable returned non-zero - retry with `gipity relay install`.'));
|
|
100
|
+
}
|
|
101
|
+
console.log(dim('In the Gipity web CLI, type `/claude` to dispatch messages to this machine.'));
|
|
102
|
+
});
|
|
19
103
|
// ─── gipity relay status ───────────────────────────────────────────────
|
|
20
104
|
relayCommand
|
|
21
105
|
.command('status')
|
|
@@ -24,10 +108,13 @@ relayCommand
|
|
|
24
108
|
.action((opts) => {
|
|
25
109
|
const s = state.loadState();
|
|
26
110
|
if (opts.json) {
|
|
27
|
-
// Redact the token - no reason for scripts to see it.
|
|
111
|
+
// Redact the token - no reason for scripts to see it. `daemon_running`
|
|
112
|
+
// lets a supervising app (the desktop client) poll liveness and decide
|
|
113
|
+
// whether to (re)spawn `relay run` without parsing the PID file itself.
|
|
28
114
|
const safe = {
|
|
29
115
|
...s,
|
|
30
116
|
device: s.device ? { ...s.device, token: '***' } : null,
|
|
117
|
+
daemon_running: state.isDaemonRunning(),
|
|
31
118
|
};
|
|
32
119
|
console.log(JSON.stringify(safe, null, 2));
|
|
33
120
|
return;
|
|
@@ -40,6 +127,7 @@ relayCommand
|
|
|
40
127
|
console.log(`${bold('Platform:')} ${s.device.platform}`);
|
|
41
128
|
console.log(`${bold('Paired:')} ${s.device.paired_at}`);
|
|
42
129
|
console.log(`${bold('Paused:')} ${s.paused ? 'yes' : 'no'}`);
|
|
130
|
+
console.log(`${bold('Running:')} ${state.isDaemonRunning() ? 'yes' : 'no'}`);
|
|
43
131
|
});
|
|
44
132
|
// ─── gipity relay run ──────────────────────────────────────────────────
|
|
45
133
|
relayCommand
|
package/dist/index.js
CHANGED
|
@@ -35,6 +35,7 @@ import { pageCommand } from './commands/page.js';
|
|
|
35
35
|
import { recordsCommand } from './commands/records.js';
|
|
36
36
|
import { fnCommand } from './commands/fn.js';
|
|
37
37
|
import { serviceCommand } from './commands/service.js';
|
|
38
|
+
import { paymentsCommand } from './commands/payments.js';
|
|
38
39
|
import { jobCommand } from './commands/job.js';
|
|
39
40
|
import { rbacCommand } from './commands/rbac.js';
|
|
40
41
|
import { auditCommand } from './commands/audit.js';
|
|
@@ -124,7 +125,7 @@ const commonGroup = [skillCommand, projectCommand, addCommand, removeCommand, de
|
|
|
124
125
|
const connectGroup = [claudeCommand, relayCommand];
|
|
125
126
|
const projectGroup = [domainCommand, statusCommand, initCommand];
|
|
126
127
|
const filesGroup = [fileCommand, syncCommand, pushCommand, uploadCommand];
|
|
127
|
-
const appBuildingGroup = [testCommand, fnCommand, serviceCommand, jobCommand, dbCommand, logsCommand, workflowCommand, realtimeCommand, rbacCommand, auditCommand, recordsCommand];
|
|
128
|
+
const appBuildingGroup = [testCommand, fnCommand, serviceCommand, paymentsCommand, jobCommand, dbCommand, logsCommand, workflowCommand, realtimeCommand, rbacCommand, auditCommand, recordsCommand];
|
|
128
129
|
const utilitiesGroup = [pageCommand, sandboxCommand, generateCommand, emailCommand, gmailCommand, locationCommand, textCommand];
|
|
129
130
|
const agentGroup = [chatCommand, memoryCommand, agentCommand, approvalCommand];
|
|
130
131
|
const setupGroup = [loginCommand, logoutCommand, tokenCommand, creditsCommand, planCommand, doctorCommand, updateCommand, uninstallCommand];
|
package/dist/knowledge.js
CHANGED
|
@@ -21,6 +21,7 @@ Templates:
|
|
|
21
21
|
- \`api\` - Backend service, webhook, data pipeline, chatbot, cron job - no frontend
|
|
22
22
|
- \`karaoke-captions\` - Forced-alignment app - karaoke captions, subtitle timing, language learning, dubbing alignment
|
|
23
23
|
- \`outreach-agent\` - AI outreach / drip-email funnel - reach a list of people with personalized, human-approved emails that auto-send on a schedule and a self-improving agent that learns from your edits
|
|
24
|
+
- \`paid-app\` - App that charges users money - SaaS subscription, paid membership, digital product store, "Pro" upgrade, paywalled content (Stripe one-time + subscriptions)
|
|
24
25
|
When unsure, default to \`web-simple\`. After adding the template, edit the generated files, then \`gipity deploy dev\`.
|
|
25
26
|
Only skip this on a build request if the user explicitly says not to.
|
|
26
27
|
|
|
@@ -38,7 +39,8 @@ Kits are reusable building blocks added to an existing app, not whole templates
|
|
|
38
39
|
- \`gipity add records\` - Registry-driven records: declare objects/fields as data, get generic CRUD functions with validation, full-text search, soft delete, ACTOR provenance, and an audit event spine - every write is transactional (row + event). Field types include relations ({id,label}), currency, emails/phones/links composites. Ships backend functions + migrations. Needs a database (web-fullstack/api template).
|
|
39
40
|
- \`gipity add views\` - Generic UI over records-kit objects: sortable/filterable table with full-text search, create/edit/delete forms with type-appropriate widgets, kanban board with drag-to-update. Renders entirely from the field registry - zero per-object UI code. Requires the records kit.
|
|
40
41
|
- \`gipity add agent-api\` - Make your app agent-operable: named API keys (kit_api_keys) let agents and scripts write through the records kit's single write path with AGENT/API actor attribution - machine writes land on the same audit spine as human edits. Requires the records kit.
|
|
41
|
-
- \`gipity add contacts\` - Source-agnostic contact data layer for lead-gen/CRM apps: import people from LinkedIn CSV + Gmail + pasted lists, resolve duplicates into one person while keeping EVERY value from every source with provenance (multi-valued attributes, never overwrites). Exact email/URL auto-merge; fuzzy name+company goes to a human merge-review queue (reversible). Re-imports detect job changes and emit signals. User-definable tags, full-text search, and a transactional event spine. Ships backend functions + migrations. Needs a database (web-fullstack/api template)
|
|
42
|
+
- \`gipity add contacts\` - Source-agnostic contact data layer for lead-gen/CRM apps: import people from LinkedIn CSV + Gmail + pasted lists, resolve duplicates into one person while keeping EVERY value from every source with provenance (multi-valued attributes, never overwrites). Exact email/URL auto-merge; fuzzy name+company goes to a human merge-review queue (reversible). Re-imports detect job changes and emit signals. User-definable tags, full-text search, and a transactional event spine. Ships backend functions + migrations. Needs a database (web-fullstack/api template).
|
|
43
|
+
- \`gipity add stripe\` - Charge your app's end-users for one-time purchases and subscriptions via Stripe. Owner connects their own Stripe account through Gipity-hosted onboarding (no API keys to paste); money lands in their account, Gipity takes a small platform fee. Ships a buy-button / pricing component, a subscription-status helper for gating UI, a webhook-verified fulfillment function, and the payments/subscriptions tables. The platform brokers checkout + signature-verified webhooks. Needs a database (web-fullstack/api template).`;
|
|
42
44
|
export const SKILLS_CONTENT = `# Gipity Integration
|
|
43
45
|
|
|
44
46
|
Gipity is the cloud platform your project runs on - hosting, databases, deployment, file storage, code execution, workflows, and monitoring. Gip is the cloud agent that runs on Gipity.
|
|
@@ -130,6 +132,7 @@ App services skills (load before calling \`/services/*\` endpoints):
|
|
|
130
132
|
- \`app-image\` - text-to-image only (no input image / editing); providers, sizes, aspect ratios
|
|
131
133
|
- \`app-llm\` - chat completions, streaming, image input
|
|
132
134
|
- \`app-location\` - user location & reverse geocoding for deployed apps (first-party - no third-party geocoder)
|
|
135
|
+
- \`app-payments\` - charge end-users real money - Stripe one-time purchases & subscriptions, via the stripe kit (gipity add stripe)
|
|
133
136
|
- \`app-realtime\` - Gipity Realtime rooms, relay vs state
|
|
134
137
|
- \`app-tts\` - voices, multi-speaker, languages
|
|
135
138
|
- \`app-video\` - Gipity Video: models, aspect, resolution
|
package/dist/relay/onboarding.js
CHANGED
|
@@ -5,43 +5,16 @@
|
|
|
5
5
|
* daemon that auto-starts on every subsequent `gipity claude` invocation,
|
|
6
6
|
* and - if they said yes to the last question - also starts at OS login.
|
|
7
7
|
*/
|
|
8
|
-
import { hostname
|
|
9
|
-
import { spawn, spawnSync } from 'child_process';
|
|
10
|
-
import { resolve, dirname } from 'path';
|
|
11
|
-
import { mkdirSync, writeFileSync } from 'fs';
|
|
12
|
-
import { post } from '../api.js';
|
|
8
|
+
import { hostname } from 'os';
|
|
13
9
|
import { prompt, confirm } from '../utils.js';
|
|
14
10
|
import { bold, brand, dim, success, error as clrError, muted, info } from '../colors.js';
|
|
15
11
|
import * as state from './state.js';
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
/**
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
return 'linux';
|
|
23
|
-
}
|
|
24
|
-
/** Path to the currently-running `gipity` CLI (for embedding in service units). */
|
|
25
|
-
function resolveCliPath() {
|
|
26
|
-
return resolve(process.argv[1] ?? 'gipity');
|
|
27
|
-
}
|
|
28
|
-
/** Spawn a fresh `gipity relay run` detached from this process. Fire-and-forget. */
|
|
29
|
-
export function ensureDaemonRunning() {
|
|
30
|
-
if (state.isDaemonRunning())
|
|
31
|
-
return;
|
|
32
|
-
try {
|
|
33
|
-
// Launch via the current Node binary + the CLI entry script. The previous
|
|
34
|
-
// `shell: true` on Windows ran the .js path through the shell, where the
|
|
35
|
-
// file association (Windows Script Host, not Node) would mis-handle it.
|
|
36
|
-
// process.execPath is cross-platform and needs no shell.
|
|
37
|
-
const child = spawn(process.execPath, [resolveCliPath(), 'relay', 'run'], {
|
|
38
|
-
detached: true,
|
|
39
|
-
stdio: 'ignore',
|
|
40
|
-
});
|
|
41
|
-
child.unref();
|
|
42
|
-
}
|
|
43
|
-
catch { /* best-effort */ }
|
|
44
|
-
}
|
|
12
|
+
import { UnsupportedPlatformError } from './installers.js';
|
|
13
|
+
import { pairDevice, startDaemon, installAutostart } from './setup.js';
|
|
14
|
+
/** Spawn a fresh `gipity relay run` detached from this process. Fire-and-forget.
|
|
15
|
+
* Re-exported from the shared `setup` core so existing importers (`claude.ts`)
|
|
16
|
+
* keep their import path. */
|
|
17
|
+
export const ensureDaemonRunning = startDaemon;
|
|
45
18
|
/**
|
|
46
19
|
* First-run prompt block. Idempotent: if the user has already answered
|
|
47
20
|
* (`relay_enabled` is a boolean), this is a no-op. Non-interactive flows
|
|
@@ -75,13 +48,11 @@ export async function maybeOfferRelayOn() {
|
|
|
75
48
|
state.setRelayEnabled(false);
|
|
76
49
|
return;
|
|
77
50
|
}
|
|
78
|
-
// Create the device directly (user-auth, no pair code).
|
|
79
|
-
|
|
80
|
-
let
|
|
51
|
+
// Create the device directly (user-auth, no pair code). `pairDevice` writes
|
|
52
|
+
// the device + flips relay_enabled on; we only have to render the outcome.
|
|
53
|
+
let device;
|
|
81
54
|
try {
|
|
82
|
-
|
|
83
|
-
token = res.data.token;
|
|
84
|
-
shortGuid = res.data.short_guid;
|
|
55
|
+
device = await pairDevice({ name });
|
|
85
56
|
}
|
|
86
57
|
catch (err) {
|
|
87
58
|
console.error(`\n ${clrError(`Could not create device: ${err?.message || err}`)}`);
|
|
@@ -89,41 +60,21 @@ export async function maybeOfferRelayOn() {
|
|
|
89
60
|
state.setRelayEnabled(false);
|
|
90
61
|
return;
|
|
91
62
|
}
|
|
92
|
-
state.setDevice({
|
|
93
|
-
guid: shortGuid,
|
|
94
|
-
name,
|
|
95
|
-
platform: mapPlatform(osPlatform()),
|
|
96
|
-
token,
|
|
97
|
-
paired_at: new Date().toISOString(),
|
|
98
|
-
});
|
|
99
|
-
state.setRelayEnabled(true);
|
|
100
63
|
// Start the daemon for this session.
|
|
101
64
|
const startNow = await confirm(' Start the relay now (and on future `gipity claude` runs)?', { default: 'yes' });
|
|
102
65
|
if (startNow) {
|
|
103
|
-
|
|
66
|
+
startDaemon();
|
|
104
67
|
}
|
|
105
68
|
// Offer OS-level autostart (launchd / systemd --user / Task Scheduler).
|
|
106
69
|
const autostartOs = await confirm(' Also start at OS login (auto-start with Windows / macOS / Linux)?', { default: 'yes' });
|
|
107
70
|
if (autostartOs) {
|
|
108
71
|
try {
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
writeFileSync(plan.path, plan.content);
|
|
112
|
-
// Run argv directly - no shell - so paths with spaces / shell metas
|
|
113
|
-
// can't break out. Fail-fast on the first non-zero exit.
|
|
114
|
-
let allOk = true;
|
|
115
|
-
for (const argv of plan.enableCmds) {
|
|
116
|
-
const r = spawnSync(argv[0], argv.slice(1), { stdio: 'ignore' });
|
|
117
|
-
if (r.status !== 0) {
|
|
118
|
-
allOk = false;
|
|
119
|
-
break;
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
if (!allOk) {
|
|
72
|
+
const res = installAutostart();
|
|
73
|
+
if (!res.ok) {
|
|
123
74
|
console.log(` ${muted('Autostart install returned non-zero - you can run')} ${brand('gipity relay install')} ${muted('later.')}`);
|
|
124
75
|
}
|
|
125
76
|
else {
|
|
126
|
-
console.log(` ${success('Auto-start installed.')} ${dim(
|
|
77
|
+
console.log(` ${success('Auto-start installed.')} ${dim(res.summary)}`);
|
|
127
78
|
}
|
|
128
79
|
}
|
|
129
80
|
catch (err) {
|
|
@@ -136,7 +87,7 @@ export async function maybeOfferRelayOn() {
|
|
|
136
87
|
}
|
|
137
88
|
}
|
|
138
89
|
console.log('');
|
|
139
|
-
console.log(` ${success(`Registered as ${bold(name)} (${
|
|
90
|
+
console.log(` ${success(`Registered as ${bold(device.name)} (${device.guid}).`)}`);
|
|
140
91
|
console.log(` ${dim('In the Gipity web CLI, type `/claude` to dispatch messages to this PC.')}`);
|
|
141
92
|
console.log('');
|
|
142
93
|
void info;
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Headless relay setup primitives — the non-interactive core shared by:
|
|
3
|
+
* - the first-run `gipity claude` onboarding (`onboarding.ts`),
|
|
4
|
+
* - the `gipity relay setup` command (installer/GUI entry point),
|
|
5
|
+
* - `gipity relay install` / `gipity relay autostart` (`relay-install.ts`).
|
|
6
|
+
*
|
|
7
|
+
* Keeping pairing + autostart here means a supervising app (e.g. the desktop
|
|
8
|
+
* onboarding client) can drive the whole relay through one code path —
|
|
9
|
+
* `pairDevice()` → `startDaemon()` / `installAutostart()` — without replaying
|
|
10
|
+
* the interactive prompts that live in `onboarding.ts`. No `console` output
|
|
11
|
+
* and no `process.exit` in this module: callers own all UX.
|
|
12
|
+
*/
|
|
13
|
+
import { spawn, spawnSync } from 'child_process';
|
|
14
|
+
import { hostname, platform as osPlatform } from 'os';
|
|
15
|
+
import { resolve, dirname } from 'path';
|
|
16
|
+
import { mkdirSync, writeFileSync } from 'fs';
|
|
17
|
+
import { post } from '../api.js';
|
|
18
|
+
import * as state from './state.js';
|
|
19
|
+
import { planFor } from './installers.js';
|
|
20
|
+
import { getMachineId } from './machine-id.js';
|
|
21
|
+
/** Normalize Node's `os.platform()` to what the backend accepts. */
|
|
22
|
+
export function mapPlatform(p) {
|
|
23
|
+
if (p === 'darwin' || p === 'linux' || p === 'win32')
|
|
24
|
+
return p;
|
|
25
|
+
return 'linux';
|
|
26
|
+
}
|
|
27
|
+
/** Absolute path to the currently-running `gipity` CLI. Embedded in service
|
|
28
|
+
* units so launchd/systemd/Task Scheduler re-launch the same binary even if
|
|
29
|
+
* PATH changes. */
|
|
30
|
+
export function resolveCliPath() {
|
|
31
|
+
return resolve(process.argv[1] ?? 'gipity');
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Create + persist a relay device, marking the relay opted-in. Idempotent
|
|
35
|
+
* unless `force`: if this machine is already paired and `!force`, the
|
|
36
|
+
* existing device is returned untouched (no server call). With `force`, the
|
|
37
|
+
* old device is revoked server-side (best-effort) before re-pairing.
|
|
38
|
+
*
|
|
39
|
+
* Throws on a bad name or if the `/remote-devices` call fails (e.g. 401 when
|
|
40
|
+
* the user isn't logged in) — callers translate that into their own UX.
|
|
41
|
+
*/
|
|
42
|
+
export async function pairDevice(opts = {}) {
|
|
43
|
+
const existing = state.getDevice();
|
|
44
|
+
if (existing && !opts.force) {
|
|
45
|
+
state.setRelayEnabled(true);
|
|
46
|
+
return {
|
|
47
|
+
guid: existing.guid,
|
|
48
|
+
name: existing.name,
|
|
49
|
+
platform: mapPlatform(existing.platform),
|
|
50
|
+
reused: true,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
if (existing && opts.force) {
|
|
54
|
+
try {
|
|
55
|
+
await post(`/remote-devices/${encodeURIComponent(existing.guid)}/revoke`, {});
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
// Best-effort: a stale server-side device is harmless; proceed to re-pair.
|
|
59
|
+
}
|
|
60
|
+
state.clearDevice();
|
|
61
|
+
}
|
|
62
|
+
const name = (opts.name?.trim() || hostname() || 'my-pc').trim();
|
|
63
|
+
if (!name || name.length > 100) {
|
|
64
|
+
throw new Error('Device name must be 1–100 non-whitespace characters.');
|
|
65
|
+
}
|
|
66
|
+
const plat = mapPlatform(osPlatform());
|
|
67
|
+
const res = await post('/remote-devices', { name, platform: plat, machine_id: getMachineId() });
|
|
68
|
+
state.setDevice({
|
|
69
|
+
guid: res.data.short_guid,
|
|
70
|
+
name,
|
|
71
|
+
platform: plat,
|
|
72
|
+
token: res.data.token,
|
|
73
|
+
paired_at: new Date().toISOString(),
|
|
74
|
+
});
|
|
75
|
+
state.setRelayEnabled(true);
|
|
76
|
+
return { guid: res.data.short_guid, name, platform: plat, reused: false };
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Spawn a fresh `gipity relay run` detached from this process. No-op if the
|
|
80
|
+
* daemon is already running. Fire-and-forget — best-effort, never throws.
|
|
81
|
+
*
|
|
82
|
+
* Launches via the current Node binary + CLI entry script (not a shell): the
|
|
83
|
+
* previous `shell: true` on Windows ran the `.js` path through Windows Script
|
|
84
|
+
* Host instead of Node. `process.execPath` is cross-platform and needs no shell.
|
|
85
|
+
*/
|
|
86
|
+
export function startDaemon() {
|
|
87
|
+
if (state.isDaemonRunning())
|
|
88
|
+
return;
|
|
89
|
+
try {
|
|
90
|
+
const child = spawn(process.execPath, [resolveCliPath(), 'relay', 'run'], {
|
|
91
|
+
detached: true,
|
|
92
|
+
stdio: 'ignore',
|
|
93
|
+
});
|
|
94
|
+
child.unref();
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
/* best-effort */
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Write the platform service-unit and enable it (start at OS login,
|
|
102
|
+
* auto-restart on crash). Throws `UnsupportedPlatformError` from `planFor` on
|
|
103
|
+
* an OS we don't generate a unit for. Returns `ok: false` if a service-manager
|
|
104
|
+
* command exits non-zero (the file is still written, so the caller can point
|
|
105
|
+
* the user at a manual retry).
|
|
106
|
+
*
|
|
107
|
+
* `stdio` defaults to `'ignore'` (silent, for headless/GUI callers); the
|
|
108
|
+
* interactive `relay install` passes `'inherit'` so the user sees
|
|
109
|
+
* launchctl/systemctl/schtasks output verbatim.
|
|
110
|
+
*/
|
|
111
|
+
export function installAutostart(opts = {}) {
|
|
112
|
+
const stdio = opts.stdio ?? 'ignore';
|
|
113
|
+
const plan = planFor({ cliPath: resolveCliPath() });
|
|
114
|
+
mkdirSync(dirname(plan.path), { recursive: true });
|
|
115
|
+
writeFileSync(plan.path, plan.content);
|
|
116
|
+
let ok = true;
|
|
117
|
+
for (const argv of plan.enableCmds) {
|
|
118
|
+
const r = spawnSync(argv[0], argv.slice(1), { stdio });
|
|
119
|
+
if (r.status !== 0) {
|
|
120
|
+
ok = false;
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return { ok, summary: plan.summary, path: plan.path };
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Best-effort removal of the OS autostart unit. Disable is not fail-fast (the
|
|
128
|
+
* service may already be stopped); we run every command and report whether all
|
|
129
|
+
* succeeded. Throws `UnsupportedPlatformError` on an unsupported OS.
|
|
130
|
+
*/
|
|
131
|
+
export function removeAutostart(opts = {}) {
|
|
132
|
+
const stdio = opts.stdio ?? 'ignore';
|
|
133
|
+
const plan = planFor({ cliPath: resolveCliPath() });
|
|
134
|
+
let ok = true;
|
|
135
|
+
for (const argv of plan.disableCmds) {
|
|
136
|
+
const r = spawnSync(argv[0], argv.slice(1), { stdio });
|
|
137
|
+
if (r.status !== 0)
|
|
138
|
+
ok = false;
|
|
139
|
+
}
|
|
140
|
+
return { ok, plan };
|
|
141
|
+
}
|
|
142
|
+
//# sourceMappingURL=setup.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gipity",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.401",
|
|
4
4
|
"description": "The full-stack platform tuned for AI agents. Database, storage, auth, functions, deploy, and drop-in kits - all agent-tuned. Pair with Claude Code or use standalone.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"gipity": "dist/updater/shim.js",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"postbuild": "node scripts/gen-build-info.mjs",
|
|
14
14
|
"dev": "tsc --watch",
|
|
15
15
|
"test": "npm run test:smoke",
|
|
16
|
-
"test:smoke": "tsc && node --test dist/__tests__/utils.test.js dist/__tests__/colors.test.js dist/__tests__/config.test.js dist/__tests__/sync.test.js dist/__tests__/sync-apply.test.js dist/__tests__/sync-lock.test.js dist/__tests__/auth-lock.test.js dist/__tests__/push-cas.test.js dist/__tests__/upload.test.js dist/__tests__/progress.test.js dist/__tests__/updater.test.js dist/__tests__/cli-smoke.test.js dist/__tests__/claude-noninteractive.test.js dist/__tests__/claude-trust.test.js dist/__tests__/relay-state.test.js dist/__tests__/relay-daemon.test.js dist/__tests__/relay-installers.test.js dist/__tests__/relay-bridge-abort.test.js dist/__tests__/relay-redact.test.js dist/__tests__/relay-machine-id.test.js dist/__tests__/stream-json.test.js dist/__tests__/relay-ingest-contract.test.js dist/__tests__/prompts.test.js dist/__tests__/capture-transcript.test.js dist/__tests__/flag-aliases.test.js dist/__tests__/client-context.test.js dist/__tests__/adopt-cwd.test.js dist/__tests__/cli-cmd-agent.test.js dist/__tests__/cli-cmd-approval.test.js dist/__tests__/cli-cmd-audit.test.js dist/__tests__/cli-cmd-chat.test.js dist/__tests__/cli-cmd-credits.test.js dist/__tests__/cli-cmd-db.test.js dist/__tests__/cli-cmd-deploy.test.js dist/__tests__/cli-cmd-domain.test.js dist/__tests__/cli-cmd-email.test.js dist/__tests__/cli-cmd-file.test.js dist/__tests__/cli-cmd-fn.test.js dist/__tests__/cli-cmd-service.test.js dist/__tests__/cli-cmd-job.test.js dist/__tests__/cli-cmd-generate.test.js dist/__tests__/cli-cmd-gmail.test.js dist/__tests__/cli-cmd-info.test.js dist/__tests__/cli-cmd-init.test.js dist/__tests__/cli-cmd-location.test.js dist/__tests__/cli-cmd-text.test.js dist/__tests__/cli-cmd-login.test.js dist/__tests__/cli-cmd-logout.test.js dist/__tests__/cli-cmd-token.test.js dist/__tests__/cli-cmd-logs.test.js dist/__tests__/cli-cmd-memory.test.js dist/__tests__/cli-cmd-page.test.js dist/__tests__/cli-cmd-plan.test.js dist/__tests__/cli-cmd-project.test.js dist/__tests__/cli-cmd-rbac.test.js dist/__tests__/cli-cmd-realtime.test.js dist/__tests__/cli-cmd-records.test.js dist/__tests__/cli-cmd-relay.test.js dist/__tests__/cli-cmd-sandbox.test.js dist/__tests__/cli-cmd-add.test.js dist/__tests__/cli-cmd-remove.test.js dist/__tests__/cli-cmd-skill.test.js dist/__tests__/cli-cmd-test.test.js dist/__tests__/cli-cmd-workflow.test.js dist/__tests__/setup-skills-block.test.js dist/__tests__/setup-hooks.test.js",
|
|
16
|
+
"test:smoke": "tsc && node --test dist/__tests__/utils.test.js dist/__tests__/colors.test.js dist/__tests__/config.test.js dist/__tests__/sync.test.js dist/__tests__/sync-apply.test.js dist/__tests__/sync-lock.test.js dist/__tests__/auth-lock.test.js dist/__tests__/push-cas.test.js dist/__tests__/upload.test.js dist/__tests__/progress.test.js dist/__tests__/updater.test.js dist/__tests__/cli-smoke.test.js dist/__tests__/claude-noninteractive.test.js dist/__tests__/claude-trust.test.js dist/__tests__/relay-state.test.js dist/__tests__/relay-daemon.test.js dist/__tests__/relay-installers.test.js dist/__tests__/relay-bridge-abort.test.js dist/__tests__/relay-redact.test.js dist/__tests__/relay-machine-id.test.js dist/__tests__/stream-json.test.js dist/__tests__/relay-ingest-contract.test.js dist/__tests__/prompts.test.js dist/__tests__/capture-transcript.test.js dist/__tests__/flag-aliases.test.js dist/__tests__/client-context.test.js dist/__tests__/adopt-cwd.test.js dist/__tests__/cli-cmd-agent.test.js dist/__tests__/cli-cmd-approval.test.js dist/__tests__/cli-cmd-audit.test.js dist/__tests__/cli-cmd-chat.test.js dist/__tests__/cli-cmd-credits.test.js dist/__tests__/cli-cmd-db.test.js dist/__tests__/cli-cmd-deploy.test.js dist/__tests__/cli-cmd-domain.test.js dist/__tests__/cli-cmd-email.test.js dist/__tests__/cli-cmd-file.test.js dist/__tests__/cli-cmd-fn.test.js dist/__tests__/cli-cmd-service.test.js dist/__tests__/cli-cmd-job.test.js dist/__tests__/cli-cmd-generate.test.js dist/__tests__/cli-cmd-gmail.test.js dist/__tests__/cli-cmd-info.test.js dist/__tests__/cli-cmd-init.test.js dist/__tests__/cli-cmd-location.test.js dist/__tests__/cli-cmd-text.test.js dist/__tests__/cli-cmd-login.test.js dist/__tests__/cli-cmd-logout.test.js dist/__tests__/cli-cmd-token.test.js dist/__tests__/cli-cmd-logs.test.js dist/__tests__/cli-cmd-memory.test.js dist/__tests__/cli-cmd-page.test.js dist/__tests__/cli-cmd-plan.test.js dist/__tests__/cli-cmd-project.test.js dist/__tests__/cli-cmd-rbac.test.js dist/__tests__/cli-cmd-realtime.test.js dist/__tests__/cli-cmd-records.test.js dist/__tests__/cli-cmd-relay.test.js dist/__tests__/cli-cmd-doctor.test.js dist/__tests__/claude-setup.test.js dist/__tests__/cli-cmd-sandbox.test.js dist/__tests__/cli-cmd-add.test.js dist/__tests__/cli-cmd-remove.test.js dist/__tests__/cli-cmd-skill.test.js dist/__tests__/cli-cmd-test.test.js dist/__tests__/cli-cmd-workflow.test.js dist/__tests__/setup-skills-block.test.js dist/__tests__/setup-hooks.test.js",
|
|
17
17
|
"test:e2e": "tsc && GIPITY_E2E=1 node --test --test-timeout=180000 dist/__tests__/cli-e2e-live.test.js dist/__tests__/cli-e2e-sync-live.test.js dist/__tests__/cli-e2e-services-media-live.test.js dist/__tests__/cli-e2e-workflow-live.test.js dist/__tests__/cli-e2e-sandbox-live.test.js dist/__tests__/cli-e2e-page-fetch-live.test.js dist/__tests__/cli-e2e-page-test-live.test.js",
|
|
18
18
|
"test:e2e:sync": "tsc && GIPITY_E2E=1 node --test --test-timeout=180000 dist/__tests__/cli-e2e-sync-live.test.js",
|
|
19
19
|
"test:e2e:sandbox": "tsc && GIPITY_E2E=1 node --test --test-timeout=180000 dist/__tests__/cli-e2e-sandbox-live.test.js"
|