gipity 1.0.306 → 1.0.318
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 +16 -13
- package/dist/__tests__/claude-noninteractive.test.js +1 -1
- package/dist/__tests__/cli-e2e-live.test.js +3 -3
- package/dist/__tests__/helpers/spawn-cli.d.ts +1 -1
- package/dist/__tests__/helpers/spawn-cli.js +2 -2
- package/dist/__tests__/prompts.test.js +3 -3
- package/dist/__tests__/push-cas.test.js +2 -2
- package/dist/__tests__/relay-bridge-abort.test.js +2 -2
- package/dist/__tests__/relay-daemon.test.js +5 -5
- package/dist/__tests__/relay-ingest-contract.test.js +2 -2
- package/dist/__tests__/relay-installers.test.js +4 -4
- package/dist/__tests__/relay-state.test.js +1 -1
- package/dist/__tests__/stream-json.test.js +2 -2
- package/dist/__tests__/sync-apply.test.js +7 -7
- package/dist/__tests__/sync-lock.test.js +4 -4
- package/dist/__tests__/sync.test.js +2 -2
- package/dist/__tests__/updater.test.js +1 -1
- package/dist/adopt-cwd.d.ts +2 -2
- package/dist/adopt-cwd.js +9 -9
- package/dist/api.d.ts +1 -1
- package/dist/api.js +1 -1
- package/dist/auth.js +1 -1
- package/dist/banner.js +29 -29
- package/dist/banner.js.map +1 -1
- package/dist/capture/sources/claude-code.d.ts +2 -2
- package/dist/capture/sources/claude-code.js +4 -4
- package/dist/colors.js +1 -1
- package/dist/commands/agent.js +18 -22
- package/dist/commands/agent.js.map +1 -1
- package/dist/commands/api.js +1 -1
- package/dist/commands/approval.d.ts +2 -0
- package/dist/commands/approval.js +117 -0
- package/dist/commands/approval.js.map +1 -0
- package/dist/commands/chat.js +40 -2
- package/dist/commands/chat.js.map +1 -1
- package/dist/commands/claude.js +19 -19
- package/dist/commands/claude.js.map +1 -1
- package/dist/commands/credits.js +2 -2
- package/dist/commands/credits.js.map +1 -1
- package/dist/commands/db.js +9 -6
- package/dist/commands/db.js.map +1 -1
- package/dist/commands/deploy.js +2 -2
- package/dist/commands/deploy.js.map +1 -1
- package/dist/commands/doctor.js +2 -2
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/domain.js +2 -2
- package/dist/commands/email.js +30 -5
- package/dist/commands/email.js.map +1 -1
- package/dist/commands/file.js +11 -21
- package/dist/commands/file.js.map +1 -1
- package/dist/commands/fn.js +2 -2
- package/dist/commands/fn.js.map +1 -1
- package/dist/commands/generate.js +2 -2
- package/dist/commands/generate.js.map +1 -1
- package/dist/commands/gmail.d.ts +2 -0
- package/dist/commands/gmail.js +85 -0
- package/dist/commands/gmail.js.map +1 -0
- package/dist/commands/init.js +2 -2
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/location.js +1 -1
- package/dist/commands/location.js.map +1 -1
- package/dist/commands/login.js +27 -13
- package/dist/commands/login.js.map +1 -1
- package/dist/commands/logout.js +7 -4
- package/dist/commands/logout.js.map +1 -1
- package/dist/commands/logs.js +2 -2
- package/dist/commands/logs.js.map +1 -1
- package/dist/commands/memory.js +8 -18
- package/dist/commands/memory.js.map +1 -1
- package/dist/commands/page-inspect.js +2 -2
- package/dist/commands/page-inspect.js.map +1 -1
- package/dist/commands/page-screenshot.js +3 -3
- package/dist/commands/page-screenshot.js.map +1 -1
- package/dist/commands/project.js +11 -26
- package/dist/commands/project.js.map +1 -1
- package/dist/commands/push.js +6 -2
- package/dist/commands/push.js.map +1 -1
- package/dist/commands/rbac.js +7 -7
- package/dist/commands/rbac.js.map +1 -1
- package/dist/commands/records.js +10 -8
- package/dist/commands/records.js.map +1 -1
- package/dist/commands/relay-install.d.ts +2 -2
- package/dist/commands/relay-install.js +2 -2
- package/dist/commands/relay-install.js.map +1 -1
- package/dist/commands/relay.d.ts +1 -1
- package/dist/commands/relay.js +15 -15
- package/dist/commands/relay.js.map +1 -1
- package/dist/commands/sandbox.js +3 -3
- package/dist/commands/sandbox.js.map +1 -1
- package/dist/commands/scaffold.js +3 -3
- package/dist/commands/scaffold.js.map +1 -1
- package/dist/commands/skill.d.ts +2 -0
- package/dist/commands/skill.js +45 -0
- package/dist/commands/skill.js.map +1 -0
- package/dist/commands/skills.js +3 -3
- package/dist/commands/skills.js.map +1 -1
- package/dist/commands/status.js +2 -2
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/sync.js +1 -1
- package/dist/commands/sync.js.map +1 -1
- package/dist/commands/test.js +5 -5
- package/dist/commands/test.js.map +1 -1
- package/dist/commands/uninstall.d.ts +2 -2
- package/dist/commands/uninstall.js +7 -7
- package/dist/commands/uninstall.js.map +1 -1
- package/dist/commands/update.js +4 -1
- package/dist/commands/update.js.map +1 -1
- package/dist/commands/upload.js +2 -2
- package/dist/commands/upload.js.map +1 -1
- package/dist/commands/workflow.js +51 -22
- package/dist/commands/workflow.js.map +1 -1
- package/dist/config.js +3 -3
- package/dist/help-skills.js +1 -1
- package/dist/helpers/command.d.ts +1 -1
- package/dist/helpers/command.js +1 -1
- package/dist/helpers/index.d.ts +2 -2
- package/dist/helpers/index.js +2 -2
- package/dist/helpers/index.js.map +1 -1
- package/dist/helpers/output.d.ts +5 -1
- package/dist/helpers/output.js +15 -2
- package/dist/helpers/output.js.map +1 -1
- package/dist/helpers/sync.d.ts +1 -1
- package/dist/helpers/sync.js +1 -1
- package/dist/hooks/capture-runner.d.ts +2 -2
- package/dist/hooks/capture-runner.js +7 -7
- package/dist/index.js +11 -9
- package/dist/index.js.map +1 -1
- package/dist/project-setup.js +2 -2
- package/dist/prompts.d.ts +12 -12
- package/dist/prompts.js +44 -44
- package/dist/provider-docs.d.ts +4 -4
- package/dist/provider-docs.js +6 -6
- package/dist/provider-docs.js.map +1 -1
- package/dist/relay/daemon.d.ts +1 -1
- package/dist/relay/daemon.js +75 -37
- package/dist/relay/daemon.js.map +1 -1
- package/dist/relay/installers.d.ts +1 -1
- package/dist/relay/installers.js +11 -11
- package/dist/relay/onboarding.js +6 -6
- package/dist/relay/state.d.ts +1 -1
- package/dist/relay/state.js +6 -6
- package/dist/relay/stream-json.d.ts +3 -3
- package/dist/relay/stream-json.js +4 -4
- package/dist/setup.d.ts +5 -5
- package/dist/setup.js +10 -10
- package/dist/sync.js +11 -11
- package/dist/updater/bootstrap.d.ts +1 -1
- package/dist/updater/bootstrap.js +2 -2
- package/dist/updater/shim.js +1 -1
- package/dist/upload.js +3 -3
- package/dist/utils.d.ts +3 -3
- package/dist/utils.js +4 -4
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# Gipity CLI
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The agent-tuned platform where AI-built apps live.
|
|
4
4
|
|
|
5
|
-
[Gipity](https://gipity.ai) is
|
|
5
|
+
[Gipity](https://gipity.ai) is the platform: hosting, databases, file storage, deployment, workflows, code execution, and monitoring. Agent-tuned from scaffold to deploy. Use standalone, or pair with Claude Code to give your local agent cloud superpowers. Any model, any infra, always your code.
|
|
6
6
|
|
|
7
|
-
This CLI connects [Claude Code](https://claude.ai/claude-code) to Gipity's cloud platform
|
|
7
|
+
This CLI connects [Claude Code](https://claude.ai/claude-code) to Gipity's cloud platform - databases, deployment, browser testing, image gen, and 50+ other capabilities your local agent doesn't have. It also syncs files so Claude Code and the Gipity web agent share the same project.
|
|
8
8
|
|
|
9
9
|
## Getting Started
|
|
10
10
|
|
|
@@ -16,7 +16,7 @@ You need **Node.js 18+** (which includes npm) and **Claude Code**.
|
|
|
16
16
|
# macOS
|
|
17
17
|
brew install node
|
|
18
18
|
|
|
19
|
-
# Windows
|
|
19
|
+
# Windows - download the installer from https://nodejs.org
|
|
20
20
|
|
|
21
21
|
# Linux (Ubuntu/Debian)
|
|
22
22
|
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - && sudo apt install -y nodejs
|
|
@@ -32,7 +32,7 @@ That's it. `claude` walks you through login, project setup, and launches Claude
|
|
|
32
32
|
|
|
33
33
|
## Updates
|
|
34
34
|
|
|
35
|
-
The CLI auto-updates in the background. After your one-time `npm install -g gipity`, every run silently checks npm for a new version and installs it into `~/.gipity/local/`
|
|
35
|
+
The CLI auto-updates in the background. After your one-time `npm install -g gipity`, every run silently checks npm for a new version and installs it into `~/.gipity/local/` - no sudo, no re-running install commands. The new version takes effect on your next invocation.
|
|
36
36
|
|
|
37
37
|
```bash
|
|
38
38
|
gipity doctor # show install version, last update check, opt-out status
|
|
@@ -75,7 +75,7 @@ That's it. You'll see:
|
|
|
75
75
|
|
|
76
76
|
If you're already logged in, it skips straight to project setup. If you already have a project in the current directory, it skips straight to launching Claude Code.
|
|
77
77
|
|
|
78
|
-
Projects live in `~/GipityProjects/{project-slug}/`
|
|
78
|
+
Projects live in `~/GipityProjects/{project-slug}/` - created automatically on first use. Any extra flags (like `--dangerously-skip-permissions`, `--model opus`, etc.) get passed through to Claude.
|
|
79
79
|
|
|
80
80
|
### The manual way
|
|
81
81
|
|
|
@@ -93,11 +93,11 @@ claude
|
|
|
93
93
|
|
|
94
94
|
This is the good part. When you run `gipity init` in a project, it sets up two hooks in `.claude/settings.json`:
|
|
95
95
|
|
|
96
|
-
**Auto-push**
|
|
96
|
+
**Auto-push** - Every time Claude Code writes or edits a file, it gets pushed to Gipity in the background. No extra steps.
|
|
97
97
|
|
|
98
|
-
**Auto-pull**
|
|
98
|
+
**Auto-pull** - Before each turn, Claude Code pulls any changes that happened remotely (like if your Gipity agent built something via chat). Claude sees what changed and can pick up where things left off.
|
|
99
99
|
|
|
100
|
-
That means Claude Code and your Gipity agent share the same files, same project, same context. You get the best of both
|
|
100
|
+
That means Claude Code and your Gipity agent share the same files, same project, same context. You get the best of both - Claude Code for hands-on coding, Gipity for autonomous agent work.
|
|
101
101
|
|
|
102
102
|
### What gets set up
|
|
103
103
|
|
|
@@ -122,7 +122,7 @@ gipity sync down # Pull remote changes
|
|
|
122
122
|
|
|
123
123
|
| Command | What it does |
|
|
124
124
|
|---------|-------------|
|
|
125
|
-
| `gipity claude` | Log in, pick a project, and launch Claude Code
|
|
125
|
+
| `gipity claude` | Log in, pick a project, and launch Claude Code - all in one |
|
|
126
126
|
| `gipity login` | Authenticate with email + verification code |
|
|
127
127
|
| `gipity init` | Set up a Gipity project and configure Claude Code |
|
|
128
128
|
| `gipity status` | Show project, agent, and auth info |
|
|
@@ -135,6 +135,7 @@ gipity sync down # Pull remote changes
|
|
|
135
135
|
| `gipity sandbox run <code>` | Execute code in a sandboxed container |
|
|
136
136
|
| `gipity project` | List, create, switch, or delete projects |
|
|
137
137
|
| `gipity agent` | List, create, switch, or configure agents |
|
|
138
|
+
| `gipity approval` | List, create, answer, or cancel pending approvals |
|
|
138
139
|
| `gipity workflow` | Manage and trigger automated workflows |
|
|
139
140
|
| `gipity file` | Browse remote files (ls, cat, tree) |
|
|
140
141
|
| `gipity scaffold [title]` | Create app structure (`--type web`, `--type 2d-game`, or `--type 3d-world`) |
|
|
@@ -146,9 +147,11 @@ gipity sync down # Pull remote changes
|
|
|
146
147
|
| `gipity rbac` | Manage RBAC policies |
|
|
147
148
|
| `gipity audit` | Query audit logs |
|
|
148
149
|
| `gipity credits` | Check your balance and usage |
|
|
149
|
-
| `gipity
|
|
150
|
+
| `gipity skill` | List and manage agent skills |
|
|
151
|
+
| `gipity chat [list\|rename\|archive\|delete]` | Manage chats (or `gipity chat <message>` to send) |
|
|
152
|
+
| `gipity gmail [send\|reply\|search\|read]` | Send/read via your own Gmail (different from `gipity email`) |
|
|
150
153
|
| `gipity domain` | Manage custom domains for deployed apps |
|
|
151
|
-
| `gipity email` | Send emails
|
|
154
|
+
| `gipity email [send]` | Send emails from the platform (gipity@gipity.ai) |
|
|
152
155
|
| `gipity generate` | Generate images, audio, or video via your agent |
|
|
153
156
|
| `gipity logout` | Sign out and clear local tokens |
|
|
154
157
|
|
|
@@ -253,7 +256,7 @@ Your login tokens. Created by `gipity login`. Tokens auto-refresh so you shouldn
|
|
|
253
256
|
|
|
254
257
|
## Questions?
|
|
255
258
|
|
|
256
|
-
Reach out anytime
|
|
259
|
+
Reach out anytime - steve@gipity.ai
|
|
257
260
|
|
|
258
261
|
This is early and moving fast. If something's broken or confusing, I want to hear about it.
|
|
259
262
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `gipity claude -p "msg"` / `--print`
|
|
2
|
+
* `gipity claude -p "msg"` / `--print` - non-interactive passthrough mode.
|
|
3
3
|
*
|
|
4
4
|
* Exercises the early-exit preconditions (must be logged in + must have a
|
|
5
5
|
* project in cwd) since the success path shells out to `claude` which isn't
|
|
@@ -20,7 +20,7 @@ const E2E_ENABLED = process.env['GIPITY_E2E'] === '1';
|
|
|
20
20
|
const API_BASE = process.env['GIPITY_E2E_API_BASE'] ?? 'https://a.gipity.ai';
|
|
21
21
|
const EMAIL = process.env['GIPITY_E2E_EMAIL'] ?? 'ec-cli-e2e@914-6.com';
|
|
22
22
|
const CODE = process.env['GIPITY_E2E_CODE'] ?? '914914';
|
|
23
|
-
// Email convention guard
|
|
23
|
+
// Email convention guard - protect against accidentally invoking real SendGrid.
|
|
24
24
|
if (E2E_ENABLED && !EMAIL.startsWith('ec')) {
|
|
25
25
|
throw new Error(`E2E test email must start with "ec" to suppress real outbound mail: got "${EMAIL}"`);
|
|
26
26
|
}
|
|
@@ -88,7 +88,7 @@ describe('cli-e2e-live', { skip: !E2E_ENABLED && 'set GIPITY_E2E=1 to run' }, ()
|
|
|
88
88
|
it('4b. deploy dev again is idempotent (no changes)', () => {
|
|
89
89
|
const r = cli(['deploy', 'dev'], { timeout: 60000 });
|
|
90
90
|
assert.equal(r.status, 0);
|
|
91
|
-
// No strict assertion on output text
|
|
91
|
+
// No strict assertion on output text - phases may say "skipped" or "ok"
|
|
92
92
|
// depending on whether checksums caught everything. Just confirm exit 0.
|
|
93
93
|
});
|
|
94
94
|
it('4c. deploy dev --only functions filters phases', () => {
|
|
@@ -137,7 +137,7 @@ describe('cli-e2e-live', { skip: !E2E_ENABLED && 'set GIPITY_E2E=1 to run' }, ()
|
|
|
137
137
|
it('9. doctor reports sane install info with auth', () => {
|
|
138
138
|
const r = cli(['doctor']);
|
|
139
139
|
assert.equal(r.status, 0);
|
|
140
|
-
assert.match(r.stdout, /Gipity CLI
|
|
140
|
+
assert.match(r.stdout, /Gipity CLI - doctor/);
|
|
141
141
|
assert.match(r.stdout, /shim version/);
|
|
142
142
|
});
|
|
143
143
|
});
|
|
@@ -21,7 +21,7 @@ export interface SpawnOptions {
|
|
|
21
21
|
export declare function runCli(args: string[], opts?: SpawnOptions): SpawnResult;
|
|
22
22
|
export declare function makeTmpHome(): string;
|
|
23
23
|
/**
|
|
24
|
-
* Async version of runCli
|
|
24
|
+
* Async version of runCli - uses `spawn` instead of `spawnSync` so the
|
|
25
25
|
* test's event loop keeps turning while the child runs. Required for any
|
|
26
26
|
* test that spins up an in-process HTTP server for the child to hit
|
|
27
27
|
* (spawnSync deadlocks because the server can't accept connections while
|
|
@@ -4,7 +4,7 @@ import { dirname, resolve } from 'path';
|
|
|
4
4
|
import { mkdtempSync } from 'fs';
|
|
5
5
|
import { tmpdir } from 'os';
|
|
6
6
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
|
-
// __dirname is dist/__tests__/helpers
|
|
7
|
+
// __dirname is dist/__tests__/helpers - CLI entry is dist/index.js
|
|
8
8
|
export const CLI_ENTRY = resolve(__dirname, '..', '..', 'index.js');
|
|
9
9
|
/**
|
|
10
10
|
* Run the built gipity CLI with a deterministic, isolated environment.
|
|
@@ -37,7 +37,7 @@ export function makeTmpHome() {
|
|
|
37
37
|
return mkdtempSync(`${tmpdir()}/gipity-cli-test-`);
|
|
38
38
|
}
|
|
39
39
|
/**
|
|
40
|
-
* Async version of runCli
|
|
40
|
+
* Async version of runCli - uses `spawn` instead of `spawnSync` so the
|
|
41
41
|
* test's event loop keeps turning while the child runs. Required for any
|
|
42
42
|
* test that spins up an in-process HTTP server for the child to hit
|
|
43
43
|
* (spawnSync deadlocks because the server can't accept connections while
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* The user's actual message must sit between the `USER_MSG_OPEN` /
|
|
5
5
|
* `USER_MSG_CLOSE` tags with no trailing instructions, and the client-side
|
|
6
6
|
* `stripPreamble` in `platform/client/src/ts/commands/claude-display.ts`
|
|
7
|
-
* must use identical tag strings
|
|
7
|
+
* must use identical tag strings - otherwise the web CLI renders the full
|
|
8
8
|
* wrap as a `claude>` turn (the historical bug: duplicate user turns
|
|
9
9
|
* rendered as walls of preamble text).
|
|
10
10
|
*/
|
|
@@ -16,7 +16,7 @@ import { dirname, resolve } from 'path';
|
|
|
16
16
|
import { USER_MSG_OPEN, USER_MSG_CLOSE, buildFreshWrap, buildResumeWrap, } from '../prompts.js';
|
|
17
17
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
18
18
|
// dist/__tests__/ → repo root depends on build layout. These tests also run
|
|
19
|
-
// against the source tree via tsx in CI
|
|
19
|
+
// against the source tree via tsx in CI - handle both by walking up until we
|
|
20
20
|
// find the platform/ sibling.
|
|
21
21
|
function repoRoot() {
|
|
22
22
|
let p = HERE;
|
|
@@ -109,7 +109,7 @@ function stripPreambleReplica(s) {
|
|
|
109
109
|
}
|
|
110
110
|
describe('stripPreamble round-trip', () => {
|
|
111
111
|
it('recovers the exact user message from buildFreshWrap output', () => {
|
|
112
|
-
const msg = 'hello world
|
|
112
|
+
const msg = 'hello world - whats 2+2 and also a newline\nplease';
|
|
113
113
|
const out = buildFreshWrap('## ctx\n- Name: foo\n- Files: empty', msg);
|
|
114
114
|
assert.equal(stripPreambleReplica(out), msg);
|
|
115
115
|
});
|
|
@@ -84,7 +84,7 @@ describe('pushFile CAS', () => {
|
|
|
84
84
|
const { pushFile } = await import('../sync.js');
|
|
85
85
|
await assert.rejects(() => pushFile(join(projectDir, 'hello.txt')), /newer version.*serverVersion=7.*gipity sync/is);
|
|
86
86
|
});
|
|
87
|
-
it('succeeds when baseline matches
|
|
87
|
+
it('succeeds when baseline matches - pushFile updates serverVersion in baseline', async () => {
|
|
88
88
|
writeFileSync(join(projectDir, '.gipity', 'sync-state.json'), JSON.stringify({
|
|
89
89
|
projectGuid: 'proj_test',
|
|
90
90
|
files: {
|
|
@@ -111,7 +111,7 @@ describe('pushFile CAS', () => {
|
|
|
111
111
|
}), { status: 200, headers: { 'content-type': 'application/json' } });
|
|
112
112
|
}
|
|
113
113
|
if (url.startsWith('https://s3.example')) {
|
|
114
|
-
// Presigned PUT
|
|
114
|
+
// Presigned PUT - return an etag.
|
|
115
115
|
return new Response('', { status: 200, headers: { etag: '"fake-etag"' } });
|
|
116
116
|
}
|
|
117
117
|
if (url.includes('/files/upload-complete')) {
|
|
@@ -29,7 +29,7 @@ describe('bridgeAbort', () => {
|
|
|
29
29
|
assert.equal(inner.signal.aborted, true);
|
|
30
30
|
assert.equal(inner.signal.reason, 'pre');
|
|
31
31
|
});
|
|
32
|
-
it('detaches cleanly
|
|
32
|
+
it('detaches cleanly - does not leak listeners across many calls', () => {
|
|
33
33
|
const outer = new AbortController();
|
|
34
34
|
// Sanity: listenerCount via public API isn't part of EventTarget, so we
|
|
35
35
|
// verify no leak by running many cycles and confirming the process does
|
|
@@ -52,7 +52,7 @@ describe('bridgeAbort', () => {
|
|
|
52
52
|
assert.equal(leakWarn, undefined, `leaked listeners: ${leakWarn}`);
|
|
53
53
|
// After all detaches, firing outer.abort must not affect any prior
|
|
54
54
|
// inners (they're out of scope / already detached). Create one more
|
|
55
|
-
// inner WITHOUT detaching and confirm it receives the abort
|
|
55
|
+
// inner WITHOUT detaching and confirm it receives the abort - proves
|
|
56
56
|
// the bridge still works after many attach/detach cycles.
|
|
57
57
|
const liveInner = new AbortController();
|
|
58
58
|
bridgeAbort(outer.signal, liveInner);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `gipity-relay` daemon
|
|
2
|
+
* `gipity-relay` daemon - full round-trip tests against an in-process mock
|
|
3
3
|
* backend. `GIPITY_RELAY_CLAUDE_CMD` is pointed at `true` / `false` / a
|
|
4
4
|
* sleep script so we can assert the daemon's ack shape for each outcome
|
|
5
5
|
* without actually shelling out to `gipity claude`.
|
|
@@ -61,7 +61,7 @@ before(async () => {
|
|
|
61
61
|
}
|
|
62
62
|
const d = pending.shift();
|
|
63
63
|
if (!d) {
|
|
64
|
-
// Brief wait then 204
|
|
64
|
+
// Brief wait then 204 - simulates a short long-poll hold.
|
|
65
65
|
await new Promise(r => setTimeout(r, 80));
|
|
66
66
|
res.statusCode = 204;
|
|
67
67
|
return res.end();
|
|
@@ -86,7 +86,7 @@ before(async () => {
|
|
|
86
86
|
after(async () => {
|
|
87
87
|
await new Promise(resolve => server.close(() => resolve()));
|
|
88
88
|
});
|
|
89
|
-
// ─── Fixture
|
|
89
|
+
// ─── Fixture - a fresh $HOME with a paired device + pre-seeded project dir ─
|
|
90
90
|
function freshHome(opts = {}) {
|
|
91
91
|
const home = mkdtempSync(join(tmpdir(), 'gipity-daemon-'));
|
|
92
92
|
const projectsRoot = join(home, 'GipityProjects');
|
|
@@ -205,11 +205,11 @@ describe('daemon: safety checks', () => {
|
|
|
205
205
|
describe('daemon: auto-bootstrap missing project dir', () => {
|
|
206
206
|
it('creates ~/GipityProjects/<slug>/ + .gipity.json when dispatch targets an unknown project', async () => {
|
|
207
207
|
resetMock();
|
|
208
|
-
// Don't preseed the project dir
|
|
208
|
+
// Don't preseed the project dir - daemon should create it.
|
|
209
209
|
const { home, projectCwd } = freshHome({ preseedProject: false });
|
|
210
210
|
pending.push(dispatchRow({ short_guid: 'rds_bootstrap', message: 'hi' }));
|
|
211
211
|
await runDaemon(home, 'true');
|
|
212
|
-
// Ack should be "done"
|
|
212
|
+
// Ack should be "done" - the dispatch ran successfully against the new dir.
|
|
213
213
|
assert.equal(acks.length, 1);
|
|
214
214
|
assert.equal(acks[0].status, 'done', `got ${acks[0].status}: ${acks[0].error}`);
|
|
215
215
|
// Directory + .gipity.json now exist with the right guid.
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Contract test: every key the daemon emits in an ingest entry must appear
|
|
3
|
-
* in the manifest below
|
|
3
|
+
* in the manifest below - which mirrors the server's `entrySchema`
|
|
4
4
|
* (platform/server/src/routes/remote-sessions.ts). If `mapEventToEntries`
|
|
5
5
|
* ever stamps a key the server doesn't accept, this test fails loudly
|
|
6
6
|
* BEFORE the daemon ships and starts 400ing in production.
|
|
7
7
|
*
|
|
8
8
|
* Why a manifest instead of importing the server's Zod schema directly:
|
|
9
9
|
* the CLI is a standalone npm package with a strict `rootDir: src`
|
|
10
|
-
* tsconfig
|
|
10
|
+
* tsconfig - a cross-workspace import won't typecheck. The manifest is
|
|
11
11
|
* the smallest thing that catches the class of bug we hit (the daemon
|
|
12
12
|
* adding a `ts` field the server stripped silently, then later rejected).
|
|
13
13
|
*
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Platform-specific service-unit generation
|
|
2
|
+
* Platform-specific service-unit generation - pure file-content checks.
|
|
3
3
|
* Actually running launchctl/systemctl/schtasks is user-driven; not tested.
|
|
4
4
|
*/
|
|
5
5
|
import { describe, it } from 'node:test';
|
|
@@ -74,12 +74,12 @@ describe('installers: enable/disable commands look right', () => {
|
|
|
74
74
|
assert.ok(p.enableCmds[1].includes('/Run'));
|
|
75
75
|
assert.ok(p.disableCmds.some(argv => argv.includes('/Delete')));
|
|
76
76
|
});
|
|
77
|
-
it('argv arrays are flat string arrays
|
|
77
|
+
it('argv arrays are flat string arrays - no shell metacharacters injected', () => {
|
|
78
78
|
// A path with spaces must stay as a single argv slot, not split by sh.
|
|
79
79
|
const cliPath = '/Users/Test User/.npm-global/bin/gipity';
|
|
80
80
|
const p = planFor({ cliPath, platformOverride: 'darwin' });
|
|
81
81
|
// The plist path also contains the homedir, which on the test runner
|
|
82
|
-
// doesn't contain spaces
|
|
82
|
+
// doesn't contain spaces - but the contract is still: argv elements
|
|
83
83
|
// are single strings, never shell-tokenized.
|
|
84
84
|
for (const argv of [...p.enableCmds, ...p.disableCmds, p.statusCmd]) {
|
|
85
85
|
for (const part of argv) {
|
|
@@ -87,7 +87,7 @@ describe('installers: enable/disable commands look right', () => {
|
|
|
87
87
|
assert.ok(!part.includes('\n'), 'argv parts should not contain newlines');
|
|
88
88
|
}
|
|
89
89
|
}
|
|
90
|
-
// CLI path is embedded in the plist content (file body), not the argv
|
|
90
|
+
// CLI path is embedded in the plist content (file body), not the argv -
|
|
91
91
|
// sanity check that didn't change.
|
|
92
92
|
assert.match(p.content, /<string>\/Users\/Test User\/\.npm-global\/bin\/gipity<\/string>/);
|
|
93
93
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Pure unit tests for the relay's stream-json parsing pipeline. No
|
|
3
|
-
* daemon, no spawn, no network
|
|
3
|
+
* daemon, no spawn, no network - just the `parseEvent`, `mapEventToEntries`,
|
|
4
4
|
* and `createLineSplitter` logic. Uses node's built-in test runner.
|
|
5
5
|
*/
|
|
6
6
|
import { describe, it } from 'node:test';
|
|
@@ -167,7 +167,7 @@ describe('createLineSplitter', () => {
|
|
|
167
167
|
assert.equal(total, 10 * chunk.length);
|
|
168
168
|
});
|
|
169
169
|
});
|
|
170
|
-
describe('mapEventToEntries
|
|
170
|
+
describe('mapEventToEntries - round trip shape matches IngestEntry union', () => {
|
|
171
171
|
it('every emitted entry has a kind field from the declared union', () => {
|
|
172
172
|
const validKinds = [
|
|
173
173
|
'attach', 'prompt', 'assistant', 'tool_use', 'tool_result', 'compact', 'system',
|
|
@@ -89,7 +89,7 @@ describe('readBaseline', () => {
|
|
|
89
89
|
assert.deepEqual(b.files, {});
|
|
90
90
|
});
|
|
91
91
|
it('returns empty baseline when projectGuid does not match current project', async () => {
|
|
92
|
-
// Someone else's baseline in our folder
|
|
92
|
+
// Someone else's baseline in our folder - must not leak.
|
|
93
93
|
writeFileSync(join(projectDir, '.gipity', 'sync-state.json'), JSON.stringify({
|
|
94
94
|
projectGuid: 'proj_OTHER',
|
|
95
95
|
files: { 'leak.txt': { size: 1, mtime: '2024', sha256: 'x', serverVersion: 1 } },
|
|
@@ -140,7 +140,7 @@ describe('conflictedCopyName', () => {
|
|
|
140
140
|
});
|
|
141
141
|
});
|
|
142
142
|
// ─── sync() end-to-end with fetch mock ────────────────────────
|
|
143
|
-
describe('sync()
|
|
143
|
+
describe('sync() - fetch-intercepted', () => {
|
|
144
144
|
it('noop when local, remote, and baseline all agree', async () => {
|
|
145
145
|
// Baseline says we have foo.txt at v=3, sha=same.
|
|
146
146
|
writeFileSync(join(projectDir, 'foo.txt'), 'content');
|
|
@@ -167,7 +167,7 @@ describe('sync() — fetch-intercepted', () => {
|
|
|
167
167
|
assert.equal(result.applied, 0);
|
|
168
168
|
assert.equal(result.plan.actions.length, 0);
|
|
169
169
|
assert.deepEqual(result.errors, []);
|
|
170
|
-
// Baseline's lastFullSync must be bumped even when no actions fired
|
|
170
|
+
// Baseline's lastFullSync must be bumped even when no actions fired -
|
|
171
171
|
// otherwise we can't tell "sync ran and everything was fine" from
|
|
172
172
|
// "sync never ran".
|
|
173
173
|
const bl = readBaseline('proj_apply');
|
|
@@ -231,7 +231,7 @@ describe('sync() — fetch-intercepted', () => {
|
|
|
231
231
|
}));
|
|
232
232
|
stubFetch(async (url) => {
|
|
233
233
|
if (url.includes('/files/tree') && !url.includes('content=tar')) {
|
|
234
|
-
// Remote has all 20 files (baseline state)
|
|
234
|
+
// Remote has all 20 files (baseline state) - client deleted all 20 locally.
|
|
235
235
|
const data = Object.entries(files).map(([path, e]) => ({
|
|
236
236
|
path, size: 1, modified: '2024', type: 'file',
|
|
237
237
|
guid: `fl_${path}`, contentHash: e.sha256, serverVersion: e.serverVersion,
|
|
@@ -375,7 +375,7 @@ describe('sync() — fetch-intercepted', () => {
|
|
|
375
375
|
});
|
|
376
376
|
it('apply-time 409 (baseline fresh but another client raced in between) → re-plans as conflict', async () => {
|
|
377
377
|
// Plan sees: local='modified', remote='unchanged' → upload with CAS=baseline.
|
|
378
|
-
// But server has already moved on between manifest-fetch and upload
|
|
378
|
+
// But server has already moved on between manifest-fetch and upload - returns
|
|
379
379
|
// 409. This exercises the apply-phase UploadConflictError handler in sync.ts.
|
|
380
380
|
const { createHash } = await import('crypto');
|
|
381
381
|
const baselineSha = createHash('sha256').update('base').digest('hex');
|
|
@@ -391,7 +391,7 @@ describe('sync() — fetch-intercepted', () => {
|
|
|
391
391
|
}));
|
|
392
392
|
let initCalls = 0;
|
|
393
393
|
stubFetch(async (url, init) => {
|
|
394
|
-
// Manifest still shows the "unchanged" remote that matches baseline
|
|
394
|
+
// Manifest still shows the "unchanged" remote that matches baseline -
|
|
395
395
|
// client will plan an upload with expected=3.
|
|
396
396
|
if (url.includes('/files/tree') && !url.includes('content=tar')) {
|
|
397
397
|
return new Response(JSON.stringify({ data: [
|
|
@@ -404,7 +404,7 @@ describe('sync() — fetch-intercepted', () => {
|
|
|
404
404
|
initCalls++;
|
|
405
405
|
const body = JSON.parse(init.body);
|
|
406
406
|
if (body.path === 'race.txt' && body.expected_server_version === 3) {
|
|
407
|
-
// Server has moved past 3
|
|
407
|
+
// Server has moved past 3 - return 409.
|
|
408
408
|
return new Response(JSON.stringify({
|
|
409
409
|
error: { code: 'CONFLICT', message: 'Version mismatch: expected 3, current 5' },
|
|
410
410
|
data: { current_server_version: 5 },
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Advisory sync lock (`.gipity/sync.lock`)
|
|
2
|
+
* Advisory sync lock (`.gipity/sync.lock`) - prevents concurrent sync
|
|
3
3
|
* processes in the same project dir from corrupting the baseline manifest.
|
|
4
4
|
*/
|
|
5
5
|
import { describe, it, before, after, beforeEach } from 'node:test';
|
|
@@ -63,7 +63,7 @@ describe('acquireLock', () => {
|
|
|
63
63
|
});
|
|
64
64
|
it('breaks a stale lock whose PID is dead', async () => {
|
|
65
65
|
const lockFile = join(tempProject, '.gipity', 'sync.lock');
|
|
66
|
-
// PID 1 exists but is init
|
|
66
|
+
// PID 1 exists but is init - but PID 999999 is almost certainly dead.
|
|
67
67
|
// Find a definitely-dead PID by forking a process that exits instantly.
|
|
68
68
|
const { execSync } = await import('child_process');
|
|
69
69
|
const deadPid = parseInt(execSync('bash -c "(echo $$; exec sleep 0.01) & wait $!; echo $!"', { encoding: 'utf-8' })
|
|
@@ -89,10 +89,10 @@ describe('acquireLock', () => {
|
|
|
89
89
|
const { acquireLock } = await import('../sync.js');
|
|
90
90
|
// First holder.
|
|
91
91
|
const releaseA = await acquireLock();
|
|
92
|
-
// Second caller must wait
|
|
92
|
+
// Second caller must wait - start its promise, then release A, then await.
|
|
93
93
|
let bResolved = false;
|
|
94
94
|
const pB = acquireLock().then(r => { bResolved = true; return r; });
|
|
95
|
-
// Give B a moment to start polling
|
|
95
|
+
// Give B a moment to start polling - it should NOT be resolved yet.
|
|
96
96
|
await new Promise(r => setTimeout(r, 100));
|
|
97
97
|
assert.equal(bResolved, false, 'second acquireLock should still be waiting');
|
|
98
98
|
releaseA();
|
|
@@ -10,7 +10,7 @@ function local(size = 100, sha) {
|
|
|
10
10
|
function baselineOf(sha, sv = 1, size = 100) {
|
|
11
11
|
return { size, mtime: '2024-01-01', sha256: sha, serverVersion: sv };
|
|
12
12
|
}
|
|
13
|
-
describe('plan()
|
|
13
|
+
describe('plan() - 9-cell decision table', () => {
|
|
14
14
|
it('unchanged × unchanged → noop (no action)', () => {
|
|
15
15
|
const p = plan(new Map([['foo', local(100, 'h1')]]), new Map([['foo', remote('foo', 'h1', 5)]]), { foo: baselineOf('h1', 5) });
|
|
16
16
|
assert.equal(p.actions.length, 0);
|
|
@@ -84,7 +84,7 @@ describe('plan() — 9-cell decision table', () => {
|
|
|
84
84
|
assert.equal(p.actions[0].kind, 'download');
|
|
85
85
|
});
|
|
86
86
|
});
|
|
87
|
-
describe('plan()
|
|
87
|
+
describe('plan() - summary counts', () => {
|
|
88
88
|
it('counts uploads, downloads, conflicts, deletes correctly', () => {
|
|
89
89
|
const p = plan(new Map([
|
|
90
90
|
['add', local(100, 'a1')], // → upload
|
|
@@ -49,7 +49,7 @@ describe('state + settings (with isolated HOME)', () => {
|
|
|
49
49
|
// by computing paths ourselves and only using state.ts's pure functions.
|
|
50
50
|
it('readState returns defaults when no file exists', async () => {
|
|
51
51
|
const mod = await import(`../updater/state.js?cachebust=${Date.now()}`);
|
|
52
|
-
// GIPITY_DIR may be cached to original HOME
|
|
52
|
+
// GIPITY_DIR may be cached to original HOME - force-create the path the
|
|
53
53
|
// module is actually using, then assert defaults shape.
|
|
54
54
|
const s = mod.readState();
|
|
55
55
|
assert.equal(typeof s.lastCheckAt, 'number');
|
package/dist/adopt-cwd.d.ts
CHANGED
|
@@ -20,7 +20,7 @@ export declare function scanForAdoption(cwd: string): AdoptScan;
|
|
|
20
20
|
* offer the "what would you like to build?" prompt so Claude can scaffold
|
|
21
21
|
* in place. */
|
|
22
22
|
export declare function isLikelyEmpty(cwd: string): boolean;
|
|
23
|
-
/** Hide-list for "Use this directory"
|
|
23
|
+
/** Hide-list for "Use this directory" - places where adopting cwd as a
|
|
24
24
|
* project is obviously wrong. Exact-match only; subdirectories are fine
|
|
25
25
|
* (e.g. `/tmp/scratch` is allowed, just not `/tmp` itself). */
|
|
26
26
|
export declare function canAdoptCwd(cwd: string): boolean;
|
|
@@ -41,7 +41,7 @@ export interface AdoptResult {
|
|
|
41
41
|
applied: number;
|
|
42
42
|
}
|
|
43
43
|
/** Adopt `cwd` as a Gipity project. Mirrors `gipity init`'s flow:
|
|
44
|
-
* 1. Slug = slugify(basename(cwd))
|
|
44
|
+
* 1. Slug = slugify(basename(cwd)) - provided by caller (so caller can
|
|
45
45
|
* validate/prompt as needed).
|
|
46
46
|
* 2. Try to match an existing server project by slug; adopt if found.
|
|
47
47
|
* 3. Otherwise POST /projects with relay-device wiring.
|
package/dist/adopt-cwd.js
CHANGED
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
* directory" picker option.
|
|
5
5
|
*
|
|
6
6
|
* The whole flow:
|
|
7
|
-
* 1. `scanForAdoption(cwd)`
|
|
7
|
+
* 1. `scanForAdoption(cwd)` - bound the size to keep us from sucking in a
|
|
8
8
|
* huge monorepo or someone's $HOME by accident.
|
|
9
|
-
* 2. `adoptCurrentDir(...)`
|
|
9
|
+
* 2. `adoptCurrentDir(...)` - find-or-create a server project keyed by the
|
|
10
10
|
* cwd's slugified basename, then `finalizeLocalProject` to write
|
|
11
11
|
* `.gipity.json`, sync, and install hooks/skills/gitignore.
|
|
12
12
|
*/
|
|
@@ -89,13 +89,13 @@ export function isLikelyEmpty(cwd) {
|
|
|
89
89
|
}
|
|
90
90
|
return entries.every(isSyncIgnored);
|
|
91
91
|
}
|
|
92
|
-
/** Hide-list for "Use this directory"
|
|
92
|
+
/** Hide-list for "Use this directory" - places where adopting cwd as a
|
|
93
93
|
* project is obviously wrong. Exact-match only; subdirectories are fine
|
|
94
94
|
* (e.g. `/tmp/scratch` is allowed, just not `/tmp` itself). */
|
|
95
95
|
export function canAdoptCwd(cwd) {
|
|
96
96
|
const norm = resolve(cwd);
|
|
97
97
|
const home = resolve(homedir());
|
|
98
|
-
// Exact root/home/system paths
|
|
98
|
+
// Exact root/home/system paths - adopting these would scoop the world.
|
|
99
99
|
const blocked = new Set([
|
|
100
100
|
sep,
|
|
101
101
|
home,
|
|
@@ -107,7 +107,7 @@ export function canAdoptCwd(cwd) {
|
|
|
107
107
|
return false;
|
|
108
108
|
// Workspace-parent heuristic: a directory at depth ≤1 below $HOME that
|
|
109
109
|
// contains 3+ subdirectories with their own `.git/` is almost certainly
|
|
110
|
-
// a parent like `~/Github/`
|
|
110
|
+
// a parent like `~/Github/` - not a single project.
|
|
111
111
|
if (norm.startsWith(home + sep)) {
|
|
112
112
|
const depth = norm.slice(home.length + 1).split(sep).length;
|
|
113
113
|
if (depth <= 1 && countGitRepoChildren(norm) >= 3)
|
|
@@ -116,7 +116,7 @@ export function canAdoptCwd(cwd) {
|
|
|
116
116
|
return true;
|
|
117
117
|
}
|
|
118
118
|
/** Count immediate subdirectories of `dir` that contain a `.git/` entry.
|
|
119
|
-
* Bounded to first 50 entries
|
|
119
|
+
* Bounded to first 50 entries - enough to flag a workspace dir without
|
|
120
120
|
* walking everything. */
|
|
121
121
|
function countGitRepoChildren(dir) {
|
|
122
122
|
let entries;
|
|
@@ -178,7 +178,7 @@ export function formatBytes(n) {
|
|
|
178
178
|
return `${(n / (1024 * 1024 * 1024)).toFixed(2)} GB`;
|
|
179
179
|
}
|
|
180
180
|
/** Adopt `cwd` as a Gipity project. Mirrors `gipity init`'s flow:
|
|
181
|
-
* 1. Slug = slugify(basename(cwd))
|
|
181
|
+
* 1. Slug = slugify(basename(cwd)) - provided by caller (so caller can
|
|
182
182
|
* validate/prompt as needed).
|
|
183
183
|
* 2. Try to match an existing server project by slug; adopt if found.
|
|
184
184
|
* 3. Otherwise POST /projects with relay-device wiring.
|
|
@@ -194,7 +194,7 @@ export async function adoptCurrentDir(opts) {
|
|
|
194
194
|
project = res.data.find(p => p.slug === opts.projectSlug) || null;
|
|
195
195
|
}
|
|
196
196
|
catch {
|
|
197
|
-
// List failed
|
|
197
|
+
// List failed - fall through to POST.
|
|
198
198
|
}
|
|
199
199
|
let isNew = false;
|
|
200
200
|
if (!project) {
|
|
@@ -215,7 +215,7 @@ export async function adoptCurrentDir(opts) {
|
|
|
215
215
|
catch (err) {
|
|
216
216
|
if (err instanceof ApiError && err.statusCode === 409) {
|
|
217
217
|
// Race: re-fetch and adopt the conflicting one (someone else just
|
|
218
|
-
// took the slug
|
|
218
|
+
// took the slug - likely the same user from another tab).
|
|
219
219
|
const res = await get('/projects?limit=100');
|
|
220
220
|
const found = res.data.find(p => p.slug === opts.projectSlug);
|
|
221
221
|
if (!found)
|
package/dist/api.d.ts
CHANGED
|
@@ -24,7 +24,7 @@ export declare function download(path: string): Promise<Buffer>;
|
|
|
24
24
|
/** Download a response as a Node.js Readable stream */
|
|
25
25
|
export declare function downloadStream(path: string): Promise<import('stream').Readable>;
|
|
26
26
|
/**
|
|
27
|
-
* PUT raw bytes to a presigned URL (no auth header
|
|
27
|
+
* PUT raw bytes to a presigned URL (no auth header - the URL is signed).
|
|
28
28
|
* Supports a Buffer or a Readable stream body. Returns the response ETag header
|
|
29
29
|
* (without quotes), used for multipart upload completion.
|
|
30
30
|
*/
|
package/dist/api.js
CHANGED
|
@@ -141,7 +141,7 @@ export async function downloadStream(path) {
|
|
|
141
141
|
return Readable.fromWeb(res.body);
|
|
142
142
|
}
|
|
143
143
|
/**
|
|
144
|
-
* PUT raw bytes to a presigned URL (no auth header
|
|
144
|
+
* PUT raw bytes to a presigned URL (no auth header - the URL is signed).
|
|
145
145
|
* Supports a Buffer or a Readable stream body. Returns the response ETag header
|
|
146
146
|
* (without quotes), used for multipart upload completion.
|
|
147
147
|
*/
|