insta 0.0.59 → 0.0.60

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.
@@ -11,24 +11,36 @@ import { renderReport } from '../observe/report.js';
11
11
  import { ApiClient, requireProject } from '../api.js';
12
12
  import { info, printJson } from '../util.js';
13
13
  // Where the audit lives: the hook records at the directory it is materialized in
14
- // (<root>/.insta/observe/hook.js — see projectRootFor), so report/sync must anchor on the same
15
- // thing. The link file is the usual root, but a standalone `insta observe install` in an
16
- // unlinked directory has no project.json — climb for the hook itself then, so both halves agree
17
- // from any subdirectory. Falls back to cwd when neither exists (→ "audit log is empty").
14
+ // (<root>/.insta/observe/hook.js — see projectRootFor), so report/sync must anchor on exactly
15
+ // that: the NEAREST materialized hook above cwd, which is what the Codex wrapper and the Claude
16
+ // entry run. The link file is only a fallback for a directory where no hook exists yet (it is
17
+ // where the next install will land), then cwd (→ "audit log is empty"). Preferring the link file
18
+ // would break the moment the two diverge — e.g. a hook materialized below a linked parent —
19
+ // because the reader would look where the writer never writes.
18
20
  export async function auditRoot(cwd = process.cwd()) {
19
- const linked = await findProjectRoot(cwd);
20
- if (linked)
21
- return linked;
21
+ const hook = nearestHookRoot(cwd);
22
+ if (hook)
23
+ return hook;
24
+ return (await findProjectRoot(cwd)) ?? cwd;
25
+ }
26
+ /** Nearest ancestor (or cwd) holding a materialized hook, or null. */
27
+ export function nearestHookRoot(cwd) {
22
28
  let dir = resolve(cwd);
23
29
  for (;;) {
24
30
  if (existsSync(join(dir, '.insta', 'observe', 'hook.js')))
25
31
  return dir;
26
32
  const parent = dirname(dir);
27
33
  if (parent === dir)
28
- return cwd;
34
+ return null;
29
35
  dir = parent;
30
36
  }
31
37
  }
38
+ /** Where `install` materializes: the linked project root when inside one (so a re-link or
39
+ * `observe install` from a subdirectory refreshes the project's hook instead of minting a second
40
+ * one the readers never look at — same rule as writeProject), else cwd. */
41
+ export async function installRoot(cwd = process.cwd()) {
42
+ return (await findProjectRoot(cwd)) ?? cwd;
43
+ }
32
44
  async function readAudit() {
33
45
  try {
34
46
  const txt = await readFile(join(await auditRoot(), '.insta', 'audit.jsonl'), 'utf8');
@@ -46,8 +58,9 @@ function* chunk(a, n) {
46
58
  yield a.slice(i, i + n);
47
59
  }
48
60
  export async function observeInstall() {
49
- const res = installObserve({ cwd: process.cwd() });
50
- info(`installed observe hook (claude: ${res.claude}, codex: ${res.codex}) → ./.insta/observe`);
61
+ const root = await installRoot();
62
+ const res = installObserve({ cwd: root });
63
+ info(`installed observe hook (claude: ${res.claude}, codex: ${res.codex}) → ${join(root, '.insta', 'observe')}`);
51
64
  if (res.ignored.length)
52
65
  info(` .gitignore += ${res.ignored.join(', ')}`);
53
66
  const hint = untrackHint(res.tracked);
@@ -57,7 +70,7 @@ export async function observeInstall() {
57
70
  info('run `insta observe report` to review, `insta observe sync` to upload to the project timeline');
58
71
  }
59
72
  export async function observeUninstall() {
60
- uninstallObserve(process.cwd());
73
+ uninstallObserve(await installRoot());
61
74
  info('uninstalled observe hook');
62
75
  }
63
76
  export async function observeReport(opts) {
@@ -3,6 +3,7 @@ import { ApiClient, requireProject } from '../api.js';
3
3
  import { writeProject } from '../config.js';
4
4
  import { info, die, printJson, handleApproval, renderNextActions } from '../util.js';
5
5
  import { installObserve } from '../observe/install.js';
6
+ import { installRoot } from './observe.js';
6
7
  import { untrackHint } from '../gitignore.js';
7
8
  import { installSkills } from '../ensure-skills.js';
8
9
  // Generic directory names that make a useless project name ("projects", "~", "tmp", …). When the
@@ -14,10 +15,12 @@ const GENERIC_DIRS = new Set([
14
15
  'app', 'apps', 'users', 'user', 'bin', 'new', 'test', 'tests',
15
16
  ]);
16
17
  // Best-effort: wire the credential-audit hook into the project (no-op if assets aren't built).
18
+ // Anchored at the linked project root (writeProject just updated it), not cwd: a re-link from a
19
+ // subdirectory must refresh the project's hook, not mint a second one the readers never see.
17
20
  // quiet: with --json the install still runs, but its note moves to stderr (stdout is JSON-only).
18
- function tryInstallObserve(quiet = false) {
21
+ async function tryInstallObserve(quiet = false) {
19
22
  try {
20
- const r = installObserve({ cwd: process.cwd() });
23
+ const r = installObserve({ cwd: await installRoot() });
21
24
  const say = (line) => (quiet ? process.stderr.write(line + '\n') : info(line));
22
25
  if (r.claude || r.codex)
23
26
  say(' installed observe hook (credential audit) → ./.insta/observe');
@@ -82,7 +85,7 @@ export async function projectCreate(name, opts) {
82
85
  info(` linked ./.insta/project.json (branch ${out.defaultBranch.name})`);
83
86
  renderNextActions(out.nextActions);
84
87
  }
85
- tryInstallObserve(opts.json);
88
+ await tryInstallObserve(opts.json);
86
89
  await installSkills({ cwd: process.cwd(), print: skillsPrint(opts.json) });
87
90
  }
88
91
  export async function projectList(opts) {
@@ -104,7 +107,7 @@ export async function projectLink(id, opts = {}) {
104
107
  printJson({ project, linked: { projectId: project.id, orgId: project.org_id, branch: 'main' } });
105
108
  else
106
109
  info(`linked project ${project.id} (${project.name})`);
107
- tryInstallObserve(opts.json);
110
+ await tryInstallObserve(opts.json);
108
111
  await installSkills({ cwd: process.cwd(), print: skillsPrint(opts.json) });
109
112
  }
110
113
  export async function projectDelete(opts) {
@@ -91,16 +91,18 @@ export async function installSkills(deps) {
91
91
  installed++;
92
92
  print(r.ok ? ` ${s.label} ✓` : ` ${s.label} failed — add manually: npx ${s.args.join(' ')}`);
93
93
  }
94
- // Only when something was actually written: an offline run that failed every add has no
95
- // skill dirs or lock to ignore, and a `.gitignore +=` line there would claim otherwise.
94
+ // The already-tracked hint is independent of this run: a repo that committed the skill dirs
95
+ // or the lock months ago needs the `git rm --cached` line whether or not today's adds worked.
96
+ const hint = untrackHint(alreadyTracked(deps.cwd, SKILL_DIRS));
97
+ if (hint)
98
+ print(hint);
99
+ // The ignore entries only when something was actually written: an offline run that failed
100
+ // every add has no new skill dirs or lock, and a `.gitignore +=` line would claim otherwise.
96
101
  if (installed === 0)
97
102
  return;
98
103
  const added = ensureGitignore(deps.cwd, SKILL_DIRS, GITIGNORE_COMMENT);
99
104
  if (added.length)
100
105
  print(` .gitignore += ${added.join(', ')}`);
101
- const hint = untrackHint(alreadyTracked(deps.cwd, SKILL_DIRS));
102
- if (hint)
103
- print(hint);
104
106
  }
105
107
  catch {
106
108
  /* best-effort convenience — never block the host command */
@@ -84,12 +84,15 @@ function claudeEntry() {
84
84
  // a fresh clone with no ./.insta anywhere above is a silent no-op. The script must stay free of
85
85
  // characters either shell rewrites inside double quotes: `$` and backticks (sh), `%` and `!`
86
86
  // (cmd.exe), and `"` (both). Codex has each user trust the entry before it runs, so shareable is safe.
87
+ // Exit code: the hook's own status when it ran; 0 when it could not be started or was killed
88
+ // (status null) — every other arm of this install is silent best-effort, and Codex reports any
89
+ // non-zero exit as a failed hook after EVERY tool call, which is worse than a missed audit line.
87
90
  const CODEX_HOOK_SCRIPT = [
88
91
  "const f=require('fs'),p=require('path'),c=require('child_process');",
89
92
  'let d=process.cwd();',
90
93
  'for(;;){',
91
94
  "const h=p.join(d,'.insta/observe/hook.js');",
92
- "if(f.existsSync(h)){const r=c.spawnSync(process.execPath,[h],{stdio:'inherit'});process.exitCode=r.status===null?1:r.status;break}",
95
+ "if(f.existsSync(h)){const r=c.spawnSync(process.execPath,[h],{stdio:'inherit'});process.exitCode=r.status===null?0:r.status;break}",
93
96
  'const u=p.dirname(d);if(u===d)break;d=u}',
94
97
  ].join('');
95
98
  function codexEntry() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "insta",
3
- "version": "0.0.59",
3
+ "version": "0.0.60",
4
4
  "type": "module",
5
5
  "description": "InstaCloud CLI — a thin client of the platform control-plane API.",
6
6
  "keywords": [