gipity 1.0.420 → 1.0.422
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/commands/deploy.js +5 -5
- package/dist/commands/sandbox.js +14 -2
- package/dist/commands/storage.js +14 -1
- package/dist/knowledge.js +1 -1
- package/dist/sync.js +40 -27
- package/package.json +1 -1
package/dist/commands/deploy.js
CHANGED
|
@@ -80,11 +80,11 @@ export const deployCommand = new Command('deploy')
|
|
|
80
80
|
const failedPhases = d.phases?.filter(p => p.status === 'failed') ?? [];
|
|
81
81
|
if (failedPhases.length > 0) {
|
|
82
82
|
// The database phase can fail on the account-wide database cap, whose
|
|
83
|
-
// server message ("Maximum of N databases reached. Drop one
|
|
84
|
-
// names no command. The droppable databases live in OTHER
|
|
85
|
-
// the default project-scoped `gipity db list` shows nothing
|
|
86
|
-
// caller straight at the account-wide list + drop path so
|
|
87
|
-
// dead-end (or reach for raw DB access) to free a slot.
|
|
83
|
+
// server message ("Maximum of N databases reached (you have N). Drop one
|
|
84
|
+
// first.") names no command. The droppable databases live in OTHER
|
|
85
|
+
// projects, so the default project-scoped `gipity db list` shows nothing
|
|
86
|
+
// — point the caller straight at the account-wide list + drop path so
|
|
87
|
+
// they don't dead-end (or reach for raw DB access) to free a slot.
|
|
88
88
|
if (failedPhases.some(p => /databases? reached|database (cap|limit)/i.test(p.summary))) {
|
|
89
89
|
console.log('');
|
|
90
90
|
console.log(muted('Free a slot under the account database cap:'));
|
package/dist/commands/sandbox.js
CHANGED
|
@@ -2,7 +2,8 @@ import { Command } from 'commander';
|
|
|
2
2
|
import { readFileSync, existsSync, statSync } from 'fs';
|
|
3
3
|
import { dirname, extname, relative } from 'path';
|
|
4
4
|
import { post } from '../api.js';
|
|
5
|
-
import { resolveProjectContext, getConfigPath } from '../config.js';
|
|
5
|
+
import { resolveProjectContext, getConfigPath, shouldIgnore } from '../config.js';
|
|
6
|
+
import { SCRATCH_IGNORE } from '../setup.js';
|
|
6
7
|
import { sync } from '../sync.js';
|
|
7
8
|
import { error as clrError, dim } from '../colors.js';
|
|
8
9
|
import { run } from '../helpers/index.js';
|
|
@@ -225,12 +226,23 @@ GCC/Rust).
|
|
|
225
226
|
const { config } = await resolveProjectContext();
|
|
226
227
|
const timeout = parseInt(opts.timeout, 10);
|
|
227
228
|
const cwd = resolveRelativeCwd();
|
|
229
|
+
// A scratch path is never synced, so it can never reach the VFS the sandbox
|
|
230
|
+
// mirrors from - `--input tmp/frame.png` would fail inside the container with
|
|
231
|
+
// a bare "no such file". Catch it here, where we can say why.
|
|
232
|
+
const scratchInputs = (opts.input ?? []).filter((p) => shouldIgnore(p.replace(/\\/g, '/').replace(/^\.\//, ''), SCRATCH_IGNORE));
|
|
233
|
+
if (scratchInputs.length) {
|
|
234
|
+
console.error(clrError(`Scratch paths are never mirrored into the sandbox: ${scratchInputs.join(', ')}`));
|
|
235
|
+
console.error(dim(` ${SCRATCH_IGNORE.join(', ')} are ignored by sync, so the sandbox never sees them.`));
|
|
236
|
+
console.error(dim(' Stage inputs at a real project path (src/, docs/, assets/) and delete them afterward.'));
|
|
237
|
+
process.exit(1);
|
|
238
|
+
}
|
|
228
239
|
// Push local working-tree changes up before executing. The sandbox mirrors
|
|
229
240
|
// the *server* (VFS), not the local cwd, so any input staged outside Claude's
|
|
230
241
|
// Write/Edit auto-push hook - a Bash `cp`/`ffmpeg`/redirect, or any external
|
|
231
242
|
// process - would otherwise be invisible to the run and the first invocation
|
|
232
243
|
// would silently miss its inputs. Syncing first makes the auto-mirror reflect
|
|
233
|
-
// local state
|
|
244
|
+
// local state however files got there - with the one exception of the scratch
|
|
245
|
+
// namespaces above, which sync ignores and so the mirror never carries.
|
|
234
246
|
// Bidirectional + CAS, so it's a cheap manifest check when nothing changed.
|
|
235
247
|
// Symmetric with the post-run pull below. Skip in one-off mode (no project).
|
|
236
248
|
if (getConfigPath()) {
|
package/dist/commands/storage.js
CHANGED
|
@@ -38,12 +38,25 @@ function printUsage(d) {
|
|
|
38
38
|
row('Dedup saved', formatBytes(s.dedupSavedBytes), `${s.dedupedObjects.toLocaleString()} shared`);
|
|
39
39
|
row('Billed', formatBytes(s.physicalBytes), 'versions kept, dedup applied');
|
|
40
40
|
if (d.projects.length > 0) {
|
|
41
|
+
// The server returns only the biggest projects. Say so, and account for the
|
|
42
|
+
// rest - a list that looks exhaustive but isn't sends people hunting for
|
|
43
|
+
// space in the wrong place.
|
|
44
|
+
const totals = d.projectTotals;
|
|
45
|
+
const hidden = totals.count - d.projects.length;
|
|
41
46
|
console.log('');
|
|
42
|
-
|
|
47
|
+
const scope = hidden > 0
|
|
48
|
+
? `(top ${d.projects.length} of ${totals.count.toLocaleString()}, live files only)`
|
|
49
|
+
: '(live files only)';
|
|
50
|
+
console.log(`${bold('By project')} ${muted(scope)}`);
|
|
43
51
|
for (const p of d.projects) {
|
|
44
52
|
const name = p.projectName ?? '(no project)';
|
|
45
53
|
row(name.length > 16 ? `${name.slice(0, 15)}…` : name, formatBytes(p.liveBytes), `${p.liveFiles.toLocaleString()} files`);
|
|
46
54
|
}
|
|
55
|
+
if (hidden > 0) {
|
|
56
|
+
const shownBytes = d.projects.reduce((n, p) => n + p.liveBytes, 0);
|
|
57
|
+
const shownFiles = d.projects.reduce((n, p) => n + p.liveFiles, 0);
|
|
58
|
+
row(`…and ${hidden.toLocaleString()} more`, formatBytes(totals.liveBytes - shownBytes), `${(totals.liveFiles - shownFiles).toLocaleString()} files`);
|
|
59
|
+
}
|
|
47
60
|
}
|
|
48
61
|
const r = d.versionRetention;
|
|
49
62
|
console.log('');
|
package/dist/knowledge.js
CHANGED
|
@@ -154,7 +154,7 @@ To keep local-only material (research clones, scratch data, vendored references)
|
|
|
154
154
|
Deploy is opt-in, not opt-out: the \`files\` phase uploads **only** what's under \`src/\` (plus \`functions/\` and \`migrations/\` as backend, not CDN files). Anything else at the project root is kept but never deployed. Put each kind of file in the right bucket so scratch and reference material can't bloat a deploy:
|
|
155
155
|
|
|
156
156
|
- **\`src/\`** - the app itself. Synced **and** deployed to the CDN. Only app code, assets, and pages belong here.
|
|
157
|
-
- **\`tmp/\`** - ephemeral scratch: file conversions, intermediate outputs, design staging. **Already ignored** (never synced, never deployed) - the one place to do throwaway work. Use this single root. (\`*_tmp/\` dirs and \`.gipityscratch/\` are auto-ignored too, as a safety net, so legacy scattered scratch like \`_vsd_tmp/\` can't leak - but write new scratch to \`tmp/\`, not scattered dirs.)
|
|
157
|
+
- **\`tmp/\`** - ephemeral scratch: file conversions, intermediate outputs, design staging. **Already ignored** (never synced, never deployed) - the one place to do throwaway work. Use this single root. (\`*_tmp/\` dirs and \`.gipityscratch/\` are auto-ignored too, as a safety net, so legacy scattered scratch like \`_vsd_tmp/\` can't leak - but write new scratch to \`tmp/\`, not scattered dirs.) Because it never syncs, \`tmp/\` is also never mirrored into the sandbox: **don't stage \`gipity sandbox run\` inputs here** - the sandbox won't see them. Stage inputs under \`src/\`/\`docs/\` and delete them after.
|
|
158
158
|
- **\`docs/\`** - reference material you want to keep: UI/architecture diagrams, design decks, notes, ADRs. Synced and versioned on the server (backed up, rollback-able) but **never deployed**, because it's outside \`src/\`. This is the home for "keep forever, don't ship" artifacts.
|
|
159
159
|
- **\`tests/\`** - \`*.test.js\` suites. Synced, run by \`gipity test\`, never deployed. \`gipity test list [path]\` lists the test files (and what a filter selects) without running them.
|
|
160
160
|
|
package/dist/sync.js
CHANGED
|
@@ -1179,6 +1179,9 @@ async function syncInner(projectGuid, root, ignore, opts, interactive) {
|
|
|
1179
1179
|
// turns "files we failed to fetch" into "delete those files." Skip ALL deletes
|
|
1180
1180
|
// this run and let a clean sync replan them once the pull succeeds.
|
|
1181
1181
|
let deletesSkippedIncomplete = 0;
|
|
1182
|
+
// Directories that lost a file to a delete-local this run. Only these are
|
|
1183
|
+
// candidates for empty-dir cleanup — see cleanupEmptyDirs.
|
|
1184
|
+
const dirsTouchedByDelete = new Set();
|
|
1182
1185
|
for (const a of plannedToApply) {
|
|
1183
1186
|
if (downloadIncomplete && (a.kind === 'delete-local' || a.kind === 'delete-remote')) {
|
|
1184
1187
|
deletesSkippedIncomplete++;
|
|
@@ -1199,11 +1202,13 @@ async function syncInner(projectGuid, root, ignore, opts, interactive) {
|
|
|
1199
1202
|
}
|
|
1200
1203
|
try {
|
|
1201
1204
|
unlinkSync(full);
|
|
1205
|
+
dirsTouchedByDelete.add(dirname(full));
|
|
1202
1206
|
delete baseline.files[a.path];
|
|
1203
1207
|
applied++;
|
|
1204
1208
|
}
|
|
1205
1209
|
catch (err) {
|
|
1206
1210
|
if (err.code === 'ENOENT') {
|
|
1211
|
+
dirsTouchedByDelete.add(dirname(full));
|
|
1207
1212
|
delete baseline.files[a.path];
|
|
1208
1213
|
applied++;
|
|
1209
1214
|
}
|
|
@@ -1273,8 +1278,8 @@ async function syncInner(projectGuid, root, ignore, opts, interactive) {
|
|
|
1273
1278
|
errors.push(`Skipped ${deletesSkippedIncomplete} deletion${deletesSkippedIncomplete === 1 ? '' : 's'} because the download was incomplete - ` +
|
|
1274
1279
|
`nothing was deleted. Re-run \`gipity sync\` once the pull finishes to apply any real deletions.`);
|
|
1275
1280
|
}
|
|
1276
|
-
// Clean up
|
|
1277
|
-
cleanupEmptyDirs(root,
|
|
1281
|
+
// Clean up local directories emptied by this run's delete-local actions.
|
|
1282
|
+
cleanupEmptyDirs(root, dirsTouchedByDelete);
|
|
1278
1283
|
// Adopt agreed content-match state into the baseline, and prune stale entries.
|
|
1279
1284
|
// `plan()` is pure, so it emits these as lists and the mutation happens here.
|
|
1280
1285
|
// Adopting (FIX M1) makes sync convergent - a locally+remotely-identical file
|
|
@@ -1296,43 +1301,51 @@ async function syncInner(projectGuid, root, ignore, opts, interactive) {
|
|
|
1296
1301
|
deferredDeletes: skippedByGuard,
|
|
1297
1302
|
};
|
|
1298
1303
|
}
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1304
|
+
/**
|
|
1305
|
+
* Remove directories that THIS run's delete-local actions left empty.
|
|
1306
|
+
*
|
|
1307
|
+
* Scoped to `emptiedDirs` on purpose. The previous version walked the whole
|
|
1308
|
+
* project tree and removed every empty directory it found, which also deleted
|
|
1309
|
+
* ones the user had just created: `mkdir -p src/images/monsters` followed a few
|
|
1310
|
+
* tool calls later by a write into it failed with ENOENT, because a full sync
|
|
1311
|
+
* had pruned the directory in between. The VFS stores files only, so a freshly
|
|
1312
|
+
* created empty directory has no remote counterpart and looked indistinguishable
|
|
1313
|
+
* from "deleted on the server."
|
|
1314
|
+
*
|
|
1315
|
+
* Pruning only where sync actually removed a file preserves the original intent
|
|
1316
|
+
* — don't leave an empty husk behind after a remote delete — while leaving every
|
|
1317
|
+
* other directory alone. An unrelated empty directory is now simply never
|
|
1318
|
+
* sync's business.
|
|
1319
|
+
*
|
|
1320
|
+
* Prunes upward, since removing `a/b/c` can leave `a/b` empty too. A directory
|
|
1321
|
+
* holding only sync-ignored entries is not empty on disk, so it survives — the
|
|
1322
|
+
* same outcome the old ignore-aware walk produced.
|
|
1323
|
+
*/
|
|
1324
|
+
function cleanupEmptyDirs(root, emptiedDirs) {
|
|
1325
|
+
const isEmpty = (dir) => {
|
|
1302
1326
|
try {
|
|
1303
|
-
|
|
1327
|
+
return readdirSync(dir).length === 0;
|
|
1304
1328
|
}
|
|
1305
1329
|
catch {
|
|
1306
1330
|
return false;
|
|
1307
1331
|
}
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
if (entry.isDirectory()) {
|
|
1317
|
-
if (!walk(full))
|
|
1318
|
-
kept++;
|
|
1319
|
-
}
|
|
1320
|
-
else {
|
|
1321
|
-
kept++;
|
|
1322
|
-
}
|
|
1323
|
-
}
|
|
1324
|
-
if (kept === 0 && dir !== root) {
|
|
1332
|
+
};
|
|
1333
|
+
// `emptiedDirs` holds dirname()s of resolveInRoot() results, which are
|
|
1334
|
+
// lexically resolved — resolve the root the same way, or the containment
|
|
1335
|
+
// guard below never matches and nothing is ever pruned.
|
|
1336
|
+
const rootResolved = resolve(root);
|
|
1337
|
+
for (const start of emptiedDirs) {
|
|
1338
|
+
let dir = resolve(start);
|
|
1339
|
+
while (dir !== rootResolved && dir.startsWith(rootResolved + sep) && isEmpty(dir)) {
|
|
1325
1340
|
try {
|
|
1326
1341
|
rmdirSync(dir);
|
|
1327
|
-
return true;
|
|
1328
1342
|
}
|
|
1329
1343
|
catch {
|
|
1330
|
-
|
|
1344
|
+
break;
|
|
1331
1345
|
}
|
|
1346
|
+
dir = dirname(dir);
|
|
1332
1347
|
}
|
|
1333
|
-
return false;
|
|
1334
1348
|
}
|
|
1335
|
-
walk(root);
|
|
1336
1349
|
}
|
|
1337
1350
|
// ─── Single-file push (used by `gipity push <file>`) ───────────
|
|
1338
1351
|
export async function pushFile(filePath) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gipity",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.422",
|
|
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",
|