codiedev 0.9.2 → 0.9.4
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/backup/cache.d.ts +14 -0
- package/dist/backup/cache.js +78 -0
- package/dist/backup/match.d.ts +2 -0
- package/dist/backup/match.js +68 -0
- package/dist/backup/run.d.ts +23 -0
- package/dist/backup/run.js +188 -0
- package/dist/commands/push.d.ts +36 -0
- package/dist/commands/push.js +59 -16
- package/dist/connect.js +3 -3
- package/dist/hook.js +19 -1
- package/dist/utils.d.ts +1 -0
- package/dist/utils.js +42 -0
- package/package.json +1 -1
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
export type HashCache = Record<string, string>;
|
|
3
|
+
export interface Candidate {
|
|
4
|
+
rel: string;
|
|
5
|
+
hash: string;
|
|
6
|
+
abs?: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function cacheKey(repoKey: string, rel: string): string;
|
|
9
|
+
export declare function loadCache(readFileSync?: typeof fs.readFileSync): HashCache;
|
|
10
|
+
export declare function saveCache(cache: HashCache, deps?: typeof fs): void;
|
|
11
|
+
export declare function applyCache(prev: HashCache, repoKey: string, candidates: Candidate[]): {
|
|
12
|
+
changed: Candidate[];
|
|
13
|
+
next: HashCache;
|
|
14
|
+
};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.cacheKey = cacheKey;
|
|
37
|
+
exports.loadCache = loadCache;
|
|
38
|
+
exports.saveCache = saveCache;
|
|
39
|
+
exports.applyCache = applyCache;
|
|
40
|
+
const fs = __importStar(require("fs"));
|
|
41
|
+
const os = __importStar(require("os"));
|
|
42
|
+
const path = __importStar(require("path"));
|
|
43
|
+
const CACHE_PATH = path.join(os.homedir(), ".codiedev", "backup-state.json");
|
|
44
|
+
function cacheKey(repoKey, rel) {
|
|
45
|
+
return `${repoKey}::${rel}`;
|
|
46
|
+
}
|
|
47
|
+
function loadCache(readFileSync = fs.readFileSync) {
|
|
48
|
+
try {
|
|
49
|
+
return JSON.parse(readFileSync(CACHE_PATH, "utf8"));
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return {}; // missing or corrupt → empty
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function saveCache(cache, deps = fs) {
|
|
56
|
+
try {
|
|
57
|
+
deps.mkdirSync(path.dirname(CACHE_PATH), { recursive: true });
|
|
58
|
+
deps.writeFileSync(CACHE_PATH, JSON.stringify(cache), { mode: 0o600 });
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
// best-effort — a failed cache write just means a redundant push next turn,
|
|
62
|
+
// which the server idempotently no-ops.
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// Pure: given the prior cache, a repo key, and this turn's candidates, return
|
|
66
|
+
// the subset whose hash changed plus the cache to persist.
|
|
67
|
+
function applyCache(prev, repoKey, candidates) {
|
|
68
|
+
const next = { ...prev };
|
|
69
|
+
const changed = [];
|
|
70
|
+
for (const c of candidates) {
|
|
71
|
+
const k = cacheKey(repoKey, c.rel);
|
|
72
|
+
if (prev[k] === c.hash)
|
|
73
|
+
continue;
|
|
74
|
+
next[k] = c.hash;
|
|
75
|
+
changed.push(c);
|
|
76
|
+
}
|
|
77
|
+
return { changed, next };
|
|
78
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Pure path matcher for the auto-backup hook. Decides whether a repo-relative
|
|
3
|
+
// path is an "artifact" worth backing up. Deny rules win over allow rules.
|
|
4
|
+
// `getContent` is lazy: only called when a path-level allow needs the
|
|
5
|
+
// skill-frontmatter exclusion (skills are pushed deliberately, not auto-backed).
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.hasSkillFrontmatter = hasSkillFrontmatter;
|
|
8
|
+
exports.isArtifactPath = isArtifactPath;
|
|
9
|
+
const DENY_BASENAMES = new Set([
|
|
10
|
+
"AGENTS.md", "CLAUDE.md", "GEMINI.md", "README.md", "CHANGELOG.md",
|
|
11
|
+
"LICENSE.md", "CONTRIBUTING.md", "notes.md",
|
|
12
|
+
]);
|
|
13
|
+
const DENY_DIR_SEGMENTS = ["node_modules/", ".git/", ".claude/", "dist/", "build/"];
|
|
14
|
+
function basename(p) {
|
|
15
|
+
const i = p.lastIndexOf("/");
|
|
16
|
+
return i === -1 ? p : p.slice(i + 1);
|
|
17
|
+
}
|
|
18
|
+
// A leading `---` block containing both `name:` and `description:` → skill.
|
|
19
|
+
function hasSkillFrontmatter(md) {
|
|
20
|
+
const m = md.match(/^?---\s*\n([\s\S]*?)\n---/);
|
|
21
|
+
if (!m)
|
|
22
|
+
return false;
|
|
23
|
+
const fm = m[1];
|
|
24
|
+
return /^\s*name\s*:/m.test(fm) && /^\s*description\s*:/m.test(fm);
|
|
25
|
+
}
|
|
26
|
+
function pathAllows(rel) {
|
|
27
|
+
const base = basename(rel);
|
|
28
|
+
if (!base.toLowerCase().endsWith(".md"))
|
|
29
|
+
return false;
|
|
30
|
+
if (/^(docs\/superpowers\/(plans|specs)\/).+\.md$/i.test(rel))
|
|
31
|
+
return true;
|
|
32
|
+
if (/(^|\/)(spec|plan|tasks)\.md$/i.test(rel))
|
|
33
|
+
return true;
|
|
34
|
+
if (/^\.specify\/.+\.md$/i.test(rel))
|
|
35
|
+
return true;
|
|
36
|
+
if (/^specs\/.+\.md$/i.test(rel))
|
|
37
|
+
return true;
|
|
38
|
+
if (/^(spec|decision|proposal|bugfix|review|rfc|adr|prd)[-_].+\.md$/i.test(base))
|
|
39
|
+
return true;
|
|
40
|
+
if (/^(SPEC|PLAN|DESIGN|PRD)\.md$/.test(base))
|
|
41
|
+
return true;
|
|
42
|
+
if (/^(ADR|RFC)-.+\.md$/i.test(base))
|
|
43
|
+
return true;
|
|
44
|
+
if (/-design\.md$/i.test(base))
|
|
45
|
+
return true;
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
function isArtifactPath(rel, getContent) {
|
|
49
|
+
const norm = rel.replace(/\\/g, "/");
|
|
50
|
+
for (const seg of DENY_DIR_SEGMENTS)
|
|
51
|
+
if (norm.includes(seg))
|
|
52
|
+
return false;
|
|
53
|
+
const base = basename(norm);
|
|
54
|
+
if (DENY_BASENAMES.has(base))
|
|
55
|
+
return false;
|
|
56
|
+
if (/^snapshot-.+\.md$/i.test(base))
|
|
57
|
+
return false;
|
|
58
|
+
if (!pathAllows(norm))
|
|
59
|
+
return false;
|
|
60
|
+
try {
|
|
61
|
+
if (hasSkillFrontmatter(getContent()))
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
// unreadable → treat as non-skill; the push will fail harmlessly later
|
|
66
|
+
}
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type HashCache, type Candidate } from "./cache";
|
|
2
|
+
export interface SelectDeps {
|
|
3
|
+
readFile: (rel: string) => string;
|
|
4
|
+
sha256: (content: string) => string;
|
|
5
|
+
}
|
|
6
|
+
export declare function selectBackupFiles(input: {
|
|
7
|
+
repoKey: string;
|
|
8
|
+
statusPaths: string[];
|
|
9
|
+
prevCache: HashCache;
|
|
10
|
+
deps: SelectDeps;
|
|
11
|
+
}): {
|
|
12
|
+
changed: Candidate[];
|
|
13
|
+
next: HashCache;
|
|
14
|
+
};
|
|
15
|
+
import { readConfig } from "../utils";
|
|
16
|
+
import { loadCache, saveCache } from "./cache";
|
|
17
|
+
export declare function runBackup(cwd: string, hookScriptPath: string): void;
|
|
18
|
+
export interface BackupUploadDeps {
|
|
19
|
+
readConfig: typeof readConfig;
|
|
20
|
+
loadCache: typeof loadCache;
|
|
21
|
+
saveCache: typeof saveCache;
|
|
22
|
+
}
|
|
23
|
+
export declare function runBackupUpload(jobFile: string, deps?: BackupUploadDeps): Promise<void>;
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.selectBackupFiles = selectBackupFiles;
|
|
37
|
+
exports.runBackup = runBackup;
|
|
38
|
+
exports.runBackupUpload = runBackupUpload;
|
|
39
|
+
const match_1 = require("./match");
|
|
40
|
+
const cache_1 = require("./cache");
|
|
41
|
+
// Pure: from this turn's git-status paths, keep artifact files whose content
|
|
42
|
+
// hash changed since the cache. Returns the changed candidates + next cache.
|
|
43
|
+
function selectBackupFiles(input) {
|
|
44
|
+
const candidates = [];
|
|
45
|
+
for (const rel of input.statusPaths) {
|
|
46
|
+
let content;
|
|
47
|
+
const getContent = () => (content ??= input.deps.readFile(rel));
|
|
48
|
+
if (!(0, match_1.isArtifactPath)(rel, getContent))
|
|
49
|
+
continue;
|
|
50
|
+
let c;
|
|
51
|
+
try {
|
|
52
|
+
c = getContent();
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
continue; // deleted/unreadable → skip
|
|
56
|
+
}
|
|
57
|
+
if (c === undefined || c === null)
|
|
58
|
+
continue;
|
|
59
|
+
candidates.push({ rel, hash: input.deps.sha256(c) });
|
|
60
|
+
}
|
|
61
|
+
return (0, cache_1.applyCache)(input.prevCache, input.repoKey, candidates);
|
|
62
|
+
}
|
|
63
|
+
const fs = __importStar(require("fs"));
|
|
64
|
+
const os = __importStar(require("os"));
|
|
65
|
+
const path = __importStar(require("path"));
|
|
66
|
+
const crypto = __importStar(require("crypto"));
|
|
67
|
+
const child_process_1 = require("child_process");
|
|
68
|
+
const utils_1 = require("../utils");
|
|
69
|
+
const cache_2 = require("./cache");
|
|
70
|
+
const push = __importStar(require("../commands/push"));
|
|
71
|
+
function gitRoot(cwd) {
|
|
72
|
+
try {
|
|
73
|
+
return (0, child_process_1.execFileSync)("git", ["-C", cwd, "rev-parse", "--show-toplevel"], {
|
|
74
|
+
encoding: "utf8",
|
|
75
|
+
}).trim() || null;
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function gitStatusPaths(root) {
|
|
82
|
+
try {
|
|
83
|
+
// --untracked-files=all is REQUIRED: the default collapses untracked files
|
|
84
|
+
// into their top-level directory (a new docs/superpowers/plans/x.md shows as
|
|
85
|
+
// just "docs/"), which would make the hook miss brand-new artifact files.
|
|
86
|
+
const out = (0, child_process_1.execFileSync)("git", ["-C", root, "status", "--porcelain", "-z", "--untracked-files=all"], { encoding: "utf8" });
|
|
87
|
+
// -z: NUL-separated, each entry "XY <path>"; rename entries have two paths.
|
|
88
|
+
return out
|
|
89
|
+
.split("\0")
|
|
90
|
+
.filter(Boolean)
|
|
91
|
+
.map((e) => e.slice(3))
|
|
92
|
+
.filter(Boolean);
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return [];
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
const sha256 = (s) => crypto.createHash("sha256").update(s, "utf8").digest("hex");
|
|
99
|
+
// Stop/sessionEnd hook entry. Best-effort, never throws.
|
|
100
|
+
function runBackup(cwd, hookScriptPath) {
|
|
101
|
+
try {
|
|
102
|
+
if (process.env.CODIEDEV_NO_AUTOBACKUP)
|
|
103
|
+
return;
|
|
104
|
+
const config = (0, utils_1.readConfig)();
|
|
105
|
+
if (!config)
|
|
106
|
+
return;
|
|
107
|
+
const root = gitRoot(cwd);
|
|
108
|
+
if (!root)
|
|
109
|
+
return;
|
|
110
|
+
const repoKey = (0, utils_1.getGitRemoteUrl)(root) ?? root;
|
|
111
|
+
const statusPaths = gitStatusPaths(root);
|
|
112
|
+
if (statusPaths.length === 0)
|
|
113
|
+
return;
|
|
114
|
+
const prevCache = (0, cache_2.loadCache)();
|
|
115
|
+
const { changed } = selectBackupFiles({
|
|
116
|
+
repoKey,
|
|
117
|
+
statusPaths,
|
|
118
|
+
prevCache,
|
|
119
|
+
deps: { readFile: (rel) => fs.readFileSync(path.join(root, rel), "utf8"), sha256 },
|
|
120
|
+
});
|
|
121
|
+
if (changed.length === 0)
|
|
122
|
+
return;
|
|
123
|
+
// The detached child owns the cache: it writes each file's hash ONLY after a
|
|
124
|
+
// successful push, so a genuine push failure (network/500) leaves the hash
|
|
125
|
+
// uncached and the file retries next turn. (Persisting here optimistically
|
|
126
|
+
// would suppress that retry, since the server no-op only covers successes.)
|
|
127
|
+
// Job carries root + repoKey + files; the child re-reads config from disk
|
|
128
|
+
// via readConfig() (same machine), avoiding any config-shape guessing.
|
|
129
|
+
const job = {
|
|
130
|
+
root,
|
|
131
|
+
repoKey,
|
|
132
|
+
files: changed.map((c) => ({ rel: c.rel, hash: c.hash })),
|
|
133
|
+
};
|
|
134
|
+
const tmpFile = path.join(os.tmpdir(), `codiedev-backup-${process.pid}.json`);
|
|
135
|
+
fs.writeFileSync(tmpFile, JSON.stringify(job), { mode: 0o600 });
|
|
136
|
+
const child = (0, child_process_1.spawn)(process.execPath, [hookScriptPath, "backup-upload", tmpFile], {
|
|
137
|
+
detached: true,
|
|
138
|
+
stdio: "ignore",
|
|
139
|
+
});
|
|
140
|
+
child.unref();
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
// best-effort
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
const DEFAULT_BACKUP_UPLOAD_DEPS = { readConfig: utils_1.readConfig, loadCache: cache_2.loadCache, saveCache: cache_2.saveCache };
|
|
147
|
+
// Detached child: push each queued file privately, caching its hash ONLY on a
|
|
148
|
+
// successful push (so a failed push retries next turn), then delete the job
|
|
149
|
+
// file. Deps are injectable so the cache write is testable without touching the
|
|
150
|
+
// global ~/.codiedev cache.
|
|
151
|
+
async function runBackupUpload(jobFile, deps = DEFAULT_BACKUP_UPLOAD_DEPS) {
|
|
152
|
+
try {
|
|
153
|
+
const job = JSON.parse(fs.readFileSync(jobFile, "utf8"));
|
|
154
|
+
const config = deps.readConfig(); // same machine — read full config from disk
|
|
155
|
+
if (!config)
|
|
156
|
+
return;
|
|
157
|
+
const cache = deps.loadCache();
|
|
158
|
+
let dirty = false;
|
|
159
|
+
for (const f of job.files) {
|
|
160
|
+
try {
|
|
161
|
+
await push.pushFile(config, {
|
|
162
|
+
absPath: path.join(job.root, f.rel),
|
|
163
|
+
key: f.rel,
|
|
164
|
+
visibility: "private",
|
|
165
|
+
contentHash: f.hash,
|
|
166
|
+
});
|
|
167
|
+
// Only now that the push landed do we record the hash — a prior failure
|
|
168
|
+
// leaves it uncached so the next turn retries it.
|
|
169
|
+
cache[(0, cache_2.cacheKey)(job.repoKey, f.rel)] = f.hash;
|
|
170
|
+
dirty = true;
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
// one file failing must not abort the rest, and its hash stays uncached
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (dirty)
|
|
177
|
+
deps.saveCache(cache);
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
// best-effort
|
|
181
|
+
}
|
|
182
|
+
finally {
|
|
183
|
+
try {
|
|
184
|
+
fs.unlinkSync(jobFile);
|
|
185
|
+
}
|
|
186
|
+
catch { /* gone */ }
|
|
187
|
+
}
|
|
188
|
+
}
|
package/dist/commands/push.d.ts
CHANGED
|
@@ -1 +1,37 @@
|
|
|
1
|
+
import { requireConfig } from "./shared";
|
|
2
|
+
interface PushResponse {
|
|
3
|
+
ok: boolean;
|
|
4
|
+
artifactId: string;
|
|
5
|
+
key: string;
|
|
6
|
+
version: number;
|
|
7
|
+
type: string;
|
|
8
|
+
kind: "doc" | "skill";
|
|
9
|
+
tags: string[];
|
|
10
|
+
destination: string;
|
|
11
|
+
workspaceName: string;
|
|
12
|
+
matchedRepo: {
|
|
13
|
+
fullName: string;
|
|
14
|
+
} | null;
|
|
15
|
+
}
|
|
16
|
+
export interface ParsedArgs {
|
|
17
|
+
file: string;
|
|
18
|
+
type?: string;
|
|
19
|
+
kind?: string;
|
|
20
|
+
tags: string[];
|
|
21
|
+
force: boolean;
|
|
22
|
+
as?: string;
|
|
23
|
+
private?: boolean;
|
|
24
|
+
}
|
|
25
|
+
export declare function parseArgs(args: string[]): ParsedArgs;
|
|
26
|
+
export declare function pushFile(config: ReturnType<typeof requireConfig>, opts: {
|
|
27
|
+
absPath: string;
|
|
28
|
+
key: string;
|
|
29
|
+
visibility?: "private" | "team";
|
|
30
|
+
contentHash?: string;
|
|
31
|
+
type?: string;
|
|
32
|
+
kind?: string;
|
|
33
|
+
tags?: string[];
|
|
34
|
+
force?: boolean;
|
|
35
|
+
}): Promise<PushResponse>;
|
|
1
36
|
export declare function runPush(args: string[]): Promise<void>;
|
|
37
|
+
export {};
|
package/dist/commands/push.js
CHANGED
|
@@ -33,9 +33,12 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.parseArgs = parseArgs;
|
|
37
|
+
exports.pushFile = pushFile;
|
|
36
38
|
exports.runPush = runPush;
|
|
37
39
|
const fs = __importStar(require("fs"));
|
|
38
40
|
const path = __importStar(require("path"));
|
|
41
|
+
const shared = __importStar(require("./shared"));
|
|
39
42
|
const shared_1 = require("./shared");
|
|
40
43
|
const utils_1 = require("../utils");
|
|
41
44
|
const scope_1 = require("../scope");
|
|
@@ -53,6 +56,8 @@ function parseArgs(args) {
|
|
|
53
56
|
let type;
|
|
54
57
|
let kind;
|
|
55
58
|
let force = false;
|
|
59
|
+
let as;
|
|
60
|
+
let priv = false;
|
|
56
61
|
const tags = [];
|
|
57
62
|
for (let i = 0; i < args.length; i++) {
|
|
58
63
|
const a = args[i];
|
|
@@ -74,15 +79,24 @@ function parseArgs(args) {
|
|
|
74
79
|
else if (a.startsWith("--tag=")) {
|
|
75
80
|
tags.push(a.slice("--tag=".length));
|
|
76
81
|
}
|
|
82
|
+
else if (a === "--as" && i + 1 < args.length) {
|
|
83
|
+
as = args[++i];
|
|
84
|
+
}
|
|
85
|
+
else if (a.startsWith("--as=")) {
|
|
86
|
+
as = a.slice("--as=".length);
|
|
87
|
+
}
|
|
77
88
|
else if (a === "--force") {
|
|
78
89
|
force = true;
|
|
79
90
|
}
|
|
91
|
+
else if (a === "--private") {
|
|
92
|
+
priv = true;
|
|
93
|
+
}
|
|
80
94
|
else if (!a.startsWith("--") && !file) {
|
|
81
95
|
file = a;
|
|
82
96
|
}
|
|
83
97
|
}
|
|
84
98
|
if (!file) {
|
|
85
|
-
console.error("Usage: codiedev push <file.md> [--kind doc|skill] [--tag <name>] [--force]");
|
|
99
|
+
console.error("Usage: codiedev push <file.md> [--as <key>] [--kind doc|skill] [--tag <name>] [--private] [--force]");
|
|
86
100
|
process.exit(1);
|
|
87
101
|
}
|
|
88
102
|
if (type && !VALID_TYPES.has(type)) {
|
|
@@ -93,10 +107,39 @@ function parseArgs(args) {
|
|
|
93
107
|
console.error(`Invalid --kind. Valid kinds: ${[...VALID_KINDS].join(", ")}`);
|
|
94
108
|
process.exit(1);
|
|
95
109
|
}
|
|
96
|
-
|
|
110
|
+
// The override key is stored verbatim by the server (docs key off it, and a
|
|
111
|
+
// path-y value like "specs/feature-a/plan.md" populates folderPath). Strip a
|
|
112
|
+
// leading slash so it can't be confused with an absolute path, and reject an
|
|
113
|
+
// empty value — but otherwise allow slashes so callers can namespace.
|
|
114
|
+
if (as !== undefined) {
|
|
115
|
+
as = as.replace(/^\/+/, "").trim();
|
|
116
|
+
if (!as) {
|
|
117
|
+
console.error("--as requires a non-empty key.");
|
|
118
|
+
process.exit(1);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return { file, type, kind, tags, force, as, private: priv };
|
|
122
|
+
}
|
|
123
|
+
async function pushFile(config, opts) {
|
|
124
|
+
const markdown = fs.readFileSync(opts.absPath, "utf8");
|
|
125
|
+
const gitRemoteUrl = (0, utils_1.getGitRemoteUrl)(process.cwd()) ?? undefined;
|
|
126
|
+
return shared.apiRequest("POST", "/api/cli/push", {
|
|
127
|
+
config,
|
|
128
|
+
body: {
|
|
129
|
+
filename: opts.key,
|
|
130
|
+
markdown,
|
|
131
|
+
type: opts.type,
|
|
132
|
+
kind: opts.kind,
|
|
133
|
+
tags: opts.tags && opts.tags.length > 0 ? opts.tags : undefined,
|
|
134
|
+
force: opts.force,
|
|
135
|
+
gitRemoteUrl,
|
|
136
|
+
visibility: opts.visibility,
|
|
137
|
+
contentHash: opts.contentHash,
|
|
138
|
+
},
|
|
139
|
+
});
|
|
97
140
|
}
|
|
98
141
|
async function runPush(args) {
|
|
99
|
-
const { file, type, kind, tags, force } = parseArgs(args);
|
|
142
|
+
const { file, type, kind, tags, force, as, private: priv } = parseArgs(args);
|
|
100
143
|
const config = (0, shared_1.requireConfig)();
|
|
101
144
|
let absolute = file;
|
|
102
145
|
if (!path.isAbsolute(absolute)) {
|
|
@@ -111,21 +154,21 @@ async function runPush(args) {
|
|
|
111
154
|
console.error(`File is empty: ${absolute}`);
|
|
112
155
|
process.exit(1);
|
|
113
156
|
}
|
|
114
|
-
|
|
115
|
-
|
|
157
|
+
// Stored key defaults to the basename (legacy behavior). `--as` overrides it
|
|
158
|
+
// so callers can namespace generically-named files — e.g. Spec Kit emits a
|
|
159
|
+
// `plan.md`/`spec.md` per feature, which would otherwise all collide on one
|
|
160
|
+
// key and silently version over each other.
|
|
161
|
+
const filename = as ?? path.basename(absolute);
|
|
116
162
|
const start = Date.now();
|
|
117
163
|
try {
|
|
118
|
-
const res = await (
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
force,
|
|
127
|
-
gitRemoteUrl,
|
|
128
|
-
},
|
|
164
|
+
const res = await pushFile(config, {
|
|
165
|
+
absPath: absolute,
|
|
166
|
+
key: filename,
|
|
167
|
+
type,
|
|
168
|
+
kind,
|
|
169
|
+
tags,
|
|
170
|
+
force,
|
|
171
|
+
visibility: priv ? "private" : undefined,
|
|
129
172
|
});
|
|
130
173
|
(0, shared_1.recordAgentEvent)(config, {
|
|
131
174
|
tool: "codiedev_push",
|
package/dist/connect.js
CHANGED
|
@@ -278,15 +278,15 @@ async function runConnect(args = []) {
|
|
|
278
278
|
}
|
|
279
279
|
};
|
|
280
280
|
if (hasClaude) {
|
|
281
|
-
runInstall("claude-code-hook", utils_1.installHook, "Claude Code SessionEnd (capture) + UserPromptSubmit (skill discovery) hooks (~/.claude/settings.json)", "claudeCode-hook");
|
|
281
|
+
runInstall("claude-code-hook", utils_1.installHook, "Claude Code SessionEnd (capture) + UserPromptSubmit (skill discovery) + Stop (auto-backup) hooks (~/.claude/settings.json)", "claudeCode-hook");
|
|
282
282
|
runInstall("claude-code-instructions", utils_1.installClaudeCodeInstructions, "Claude Code agent instructions (~/.claude/CLAUDE.md)", "claudeCode-instructions");
|
|
283
283
|
}
|
|
284
284
|
if (hasCodex) {
|
|
285
|
-
runInstall("codex-hook", utils_1.installCodexHook, "Codex Stop
|
|
285
|
+
runInstall("codex-hook", utils_1.installCodexHook, "Codex Stop (capture + auto-backup) hooks (~/.codex/hooks.json)", "codex-hook");
|
|
286
286
|
runInstall("codex-instructions", utils_1.installCodexInstructions, "Codex agent instructions (~/.codex/AGENTS.md)", "codex-instructions");
|
|
287
287
|
}
|
|
288
288
|
if (hasCursor) {
|
|
289
|
-
runInstall("cursor-hook", utils_1.installCursorHook, "Cursor sessionEnd
|
|
289
|
+
runInstall("cursor-hook", utils_1.installCursorHook, "Cursor sessionEnd (capture + auto-backup) hooks (~/.cursor/hooks.json)", "cursor-hook");
|
|
290
290
|
runInstall("cursor-instructions", utils_1.installCursorInstructions, "Cursor agent instructions (~/.cursor/rules/codiedev.mdc)", "cursor-instructions");
|
|
291
291
|
}
|
|
292
292
|
// Tell the backend which targets we wired up so the portal's onboarding
|
package/dist/hook.js
CHANGED
|
@@ -172,6 +172,14 @@ async function main() {
|
|
|
172
172
|
}
|
|
173
173
|
process.exit(0);
|
|
174
174
|
}
|
|
175
|
+
// Detached backup uploader child (spawned by runBackup below). The job file
|
|
176
|
+
// carries only { root, files }; the child re-reads config from disk. No stdin
|
|
177
|
+
// needed, so handle it before readStdin() like the capture uploader above.
|
|
178
|
+
if (process.argv[2] === "backup-upload") {
|
|
179
|
+
const { runBackupUpload } = await Promise.resolve().then(() => __importStar(require("./backup/run")));
|
|
180
|
+
await runBackupUpload(process.argv[3]);
|
|
181
|
+
process.exit(0);
|
|
182
|
+
}
|
|
175
183
|
let hookInput = {};
|
|
176
184
|
try {
|
|
177
185
|
const raw = await readStdin();
|
|
@@ -189,12 +197,22 @@ async function main() {
|
|
|
189
197
|
}
|
|
190
198
|
const mode = resolveMode(process.argv[2]);
|
|
191
199
|
// Self-heal: a user who upgraded the CLI but hasn't re-run `connect` gets the
|
|
192
|
-
// discovery
|
|
200
|
+
// discovery + auto-backup hooks wired on their next SessionEnd, without
|
|
201
|
+
// re-connecting.
|
|
193
202
|
(0, utils_1.ensureDiscoveryHookInstalled)();
|
|
203
|
+
(0, utils_1.ensureBackupHookInstalled)();
|
|
194
204
|
const { session_id, transcript_path, cwd, workspace_roots } = hookInput;
|
|
195
205
|
const workspaceRoot = workspace_roots && workspace_roots.length > 0
|
|
196
206
|
? workspace_roots[0]
|
|
197
207
|
: cwd || process.cwd();
|
|
208
|
+
// Stop/sessionEnd backup path — separate from session capture and needs no
|
|
209
|
+
// transcript. Lists changed artifact files in the workspace and dispatches a
|
|
210
|
+
// detached child that backs them up privately. Best-effort, never blocks.
|
|
211
|
+
if (process.argv[2] === "backup") {
|
|
212
|
+
const { runBackup } = await Promise.resolve().then(() => __importStar(require("./backup/run")));
|
|
213
|
+
runBackup(workspaceRoot, process.argv[1]);
|
|
214
|
+
process.exit(0);
|
|
215
|
+
}
|
|
198
216
|
const config = (0, utils_1.readConfig)();
|
|
199
217
|
if (!config) {
|
|
200
218
|
process.exit(0);
|
package/dist/utils.d.ts
CHANGED
|
@@ -38,6 +38,7 @@ export declare function vscodeCopilotInstalled(): boolean;
|
|
|
38
38
|
export declare function upsertCodiedevHook(hooks: Record<string, unknown>, eventName: string, subcommand: string): void;
|
|
39
39
|
export declare function installHook(): void;
|
|
40
40
|
export declare function ensureDiscoveryHookInstalled(): void;
|
|
41
|
+
export declare function ensureBackupHookInstalled(): void;
|
|
41
42
|
export declare function installClaudeCodeInstructions(): void;
|
|
42
43
|
export declare function installCodexInstructions(): void;
|
|
43
44
|
export declare function installCursorInstructions(): void;
|
package/dist/utils.js
CHANGED
|
@@ -46,6 +46,7 @@ exports.vscodeCopilotInstalled = vscodeCopilotInstalled;
|
|
|
46
46
|
exports.upsertCodiedevHook = upsertCodiedevHook;
|
|
47
47
|
exports.installHook = installHook;
|
|
48
48
|
exports.ensureDiscoveryHookInstalled = ensureDiscoveryHookInstalled;
|
|
49
|
+
exports.ensureBackupHookInstalled = ensureBackupHookInstalled;
|
|
49
50
|
exports.installClaudeCodeInstructions = installClaudeCodeInstructions;
|
|
50
51
|
exports.installCodexInstructions = installCodexInstructions;
|
|
51
52
|
exports.installCursorInstructions = installCursorInstructions;
|
|
@@ -274,6 +275,8 @@ function installHook() {
|
|
|
274
275
|
upsertCodiedevHook(hooks, "SessionEnd", "capture");
|
|
275
276
|
// UserPromptSubmit → discover (surface a matching team skill for the task).
|
|
276
277
|
upsertCodiedevHook(hooks, "UserPromptSubmit", "discover");
|
|
278
|
+
// Stop → backup (auto-back-up changed artifact files to your private library).
|
|
279
|
+
upsertCodiedevHook(hooks, "Stop", "backup");
|
|
277
280
|
if (!fs.existsSync(CLAUDE_DIR)) {
|
|
278
281
|
fs.mkdirSync(CLAUDE_DIR, { recursive: true });
|
|
279
282
|
}
|
|
@@ -306,6 +309,29 @@ function ensureDiscoveryHookInstalled() {
|
|
|
306
309
|
// Best-effort — never block a session on self-heal.
|
|
307
310
|
}
|
|
308
311
|
}
|
|
312
|
+
// Self-heal: ensure the Stop → backup hook is present without a full `connect`
|
|
313
|
+
// re-run, mirroring ensureDiscoveryHookInstalled. Called from the capture hook
|
|
314
|
+
// so an upgraded-but-not-reconnected user gets auto-backup on their next
|
|
315
|
+
// session. Only writes when missing, to avoid churning settings.json every run.
|
|
316
|
+
function ensureBackupHookInstalled() {
|
|
317
|
+
try {
|
|
318
|
+
if (!fs.existsSync(CLAUDE_SETTINGS_PATH))
|
|
319
|
+
return;
|
|
320
|
+
const settings = JSON.parse(fs.readFileSync(CLAUDE_SETTINGS_PATH, "utf8"));
|
|
321
|
+
const hooks = (settings.hooks ?? {});
|
|
322
|
+
const stop = (hooks.Stop ?? []);
|
|
323
|
+
const present = stop.some((h) => {
|
|
324
|
+
const inner = h.hooks;
|
|
325
|
+
return Array.isArray(inner) && inner.some((x) => (0, detection_1.isCodiedevHookCommand)(x.command));
|
|
326
|
+
});
|
|
327
|
+
if (present)
|
|
328
|
+
return;
|
|
329
|
+
settings.hooks = hooks;
|
|
330
|
+
upsertCodiedevHook(hooks, "Stop", "backup");
|
|
331
|
+
fs.writeFileSync(CLAUDE_SETTINGS_PATH, JSON.stringify(settings, null, 2), "utf8");
|
|
332
|
+
}
|
|
333
|
+
catch { /* best-effort */ }
|
|
334
|
+
}
|
|
309
335
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
310
336
|
// MCP server installation — writes the codiedev MCP server config into each
|
|
311
337
|
// agent CLI's expected location so "npx codiedev connect" wires it up once.
|
|
@@ -731,6 +757,16 @@ function installCodexHook() {
|
|
|
731
757
|
},
|
|
732
758
|
],
|
|
733
759
|
});
|
|
760
|
+
// Stop → backup: auto-back-up changed artifact files to the private library.
|
|
761
|
+
filtered.push({
|
|
762
|
+
hooks: [
|
|
763
|
+
{
|
|
764
|
+
type: "command",
|
|
765
|
+
command: resolveHookCommand("backup"),
|
|
766
|
+
timeout: 30,
|
|
767
|
+
},
|
|
768
|
+
],
|
|
769
|
+
});
|
|
734
770
|
hooks.Stop = filtered;
|
|
735
771
|
if (!fs.existsSync(CODEX_DIR)) {
|
|
736
772
|
fs.mkdirSync(CODEX_DIR, { recursive: true });
|
|
@@ -772,6 +808,12 @@ function installCursorHook() {
|
|
|
772
808
|
type: "command",
|
|
773
809
|
timeout: 30,
|
|
774
810
|
});
|
|
811
|
+
// sessionEnd → backup: auto-back-up changed artifact files to the private library.
|
|
812
|
+
filtered.push({
|
|
813
|
+
command: resolveHookCommand("backup"),
|
|
814
|
+
type: "command",
|
|
815
|
+
timeout: 30,
|
|
816
|
+
});
|
|
775
817
|
hooks.sessionEnd = filtered;
|
|
776
818
|
fs.writeFileSync(CURSOR_HOOKS_PATH, JSON.stringify(hooksFile, null, 2), "utf8");
|
|
777
819
|
}
|
package/package.json
CHANGED